VYPR
Critical severity9.8NVD Advisory· Published Jun 8, 2026

CVE-2026-52778

CVE-2026-52778

Description

YesWiki is a wiki system written in PHP. Prior to version 4.6.6, an unsafe execution vulnerability exists in the Bazar form field calculator (CalcField.php) of YesWiki. The application attempts to sanitize user-defined mathematical formulas using a complex recursive regular expression before passing them to the PHP eval() function. This implementation is inherently flawed: it is vulnerable to Regular Expression Denial of Service (ReDoS / Stack Overflow) which can crash the server, and it creates a high-risk architecture where any logic bypass directly results in arbitrary PHP code execution. Version 4.6.6 patches the issue.

Affected products

2
  • Yeswiki/Yeswikireferences2 versions
    (expand)+ 1 more
    • (no CPE)
    • (no CPE)range: <4.6.6

Patches

1
dd2bd8fb099d

fix(bazar): GHSA-px5m-h76g-p7p8

https://github.com/YesWiki/yeswikiFlorian SchmittMay 25, 2026via nvd-ref
1 file changed · +200 10
  • tools/bazar/fields/CalcField.php+200 10 modified
    @@ -13,11 +13,27 @@ class CalcField extends BazarField
     {
         protected const FIELD_DISPLAY_TEXT = 4;
         protected const FIELD_CALCFORMULA = 5;
    +
    +    private const ALLOWED_FUNCTIONS = [
    +        'sin' => 'sin', 'sinh' => 'sinh',
    +        'cos' => 'cos', 'cosh' => 'cosh',
    +        'tan' => 'tan', 'tanh' => 'tanh',
    +        'asin' => 'asin', 'asinh' => 'asinh',
    +        'acos' => 'acos', 'acosh' => 'acosh',
    +        'atan' => 'atan', 'atanh' => 'atanh',
    +        'abs' => 'abs', 'exp' => 'exp', 'log10' => 'log10',
    +        'deg2rad' => 'deg2rad', 'rad2deg' => 'rad2deg',
    +        'sqrt' => 'sqrt', 'ceil' => 'ceil', 'floor' => 'floor', 'round' => 'round',
    +    ];
    +
         protected $calcFormula;
         protected $displayText;
     
         protected $formManager;
     
    +    private array $formulaTokens = [];
    +    private int $formulaPos = 0;
    +
         public function __construct(array $values, ContainerInterface $services)
         {
             parent::__construct($values, $services);
    @@ -79,18 +95,13 @@ public function formatValuesBeforeSave($entry)
                     }
                 }
                 $formula = preg_replace('/\s+/', '', $formula);
    -            $regexpToCheckIfMathFormula = '/^((' . $number . '|' . $functions . '\s*\((?1)+\)|\((?1)+\))(?:' . $operators . '(?1))?)+$/';
    -            // Final regexp, heavily using recursive patterns
    -            if (preg_match($regexpToCheckIfMathFormula, $formula)) {
    -                $formula = preg_replace('!pi|π!', 'pi()', $formula);
    -                try {
    -                    eval("\$value = $formula;");
    -                    $value = $value ?? 0;
    -                } catch (Throwable $th) {
    +            try {
    +                $value = $this->evaluateFormula($formula);
    +                if (!is_finite($value)) {
                         $value = 0;
                     }
    -            } else {
    -                $value = 'formula not correct !';
    +            } catch (Throwable $th) {
    +                $value = 0;
                 }
             }
             if (empty($value)) {
    @@ -142,6 +153,185 @@ private function getPropertyNameIfDefined($entry, $name): ?string
             return null;
         }
     
    +    private function evaluateFormula(string $formula): float
    +    {
    +        $this->formulaTokens = $this->tokenizeFormula($formula);
    +        $this->formulaPos = 0;
    +        $result = $this->parseAddSub();
    +        if ($this->formulaPos < count($this->formulaTokens)) {
    +            throw new \RuntimeException('Unexpected token at position ' . $this->formulaPos);
    +        }
    +        return $result;
    +    }
    +
    +    private function tokenizeFormula(string $formula): array
    +    {
    +        $tokens = [];
    +        $i = 0;
    +        $len = strlen($formula);
    +        while ($i < $len) {
    +            $c = $formula[$i];
    +            if ($c === ' ' || $c === "\t") {
    +                $i++;
    +                continue;
    +            }
    +            if (ctype_digit($c) || $c === '.') {
    +                $j = $i;
    +                while ($j < $len && (ctype_digit($formula[$j]) || $formula[$j] === '.' || $formula[$j] === ',')) {
    +                    $j++;
    +                }
    +                // scientific notation (e.g. 1.5E+20)
    +                if ($j < $len && in_array($formula[$j], ['e', 'E'], true)) {
    +                    $j++;
    +                    if ($j < $len && in_array($formula[$j], ['+', '-'], true)) {
    +                        $j++;
    +                    }
    +                    while ($j < $len && ctype_digit($formula[$j])) {
    +                        $j++;
    +                    }
    +                }
    +                $tokens[] = ['type' => 'number', 'value' => (float) str_replace(',', '.', substr($formula, $i, $j - $i))];
    +                $i = $j;
    +                continue;
    +            }
    +            if (in_array($c, ['+', '-', '*', '/', '^', '%', '(', ')'], true)) {
    +                $tokens[] = ['type' => 'op', 'value' => $c];
    +                $i++;
    +                continue;
    +            }
    +            // UTF-8 π (U+03C0 = bytes 0xCF 0x80)
    +            if (ord($c) === 0xCF && $i + 1 < $len && ord($formula[$i + 1]) === 0x80) {
    +                $tokens[] = ['type' => 'name', 'value' => 'pi'];
    +                $i += 2;
    +                continue;
    +            }
    +            if (ctype_alpha($c) || $c === '_') {
    +                $j = $i;
    +                while ($j < $len && (ctype_alnum($formula[$j]) || $formula[$j] === '_')) {
    +                    $j++;
    +                }
    +                $tokens[] = ['type' => 'name', 'value' => substr($formula, $i, $j - $i)];
    +                $i = $j;
    +                continue;
    +            }
    +            throw new \RuntimeException("Unexpected character '$c' in formula");
    +        }
    +        return $tokens;
    +    }
    +
    +    private function peekToken(): ?array
    +    {
    +        return $this->formulaTokens[$this->formulaPos] ?? null;
    +    }
    +
    +    private function consumeToken(): array
    +    {
    +        $t = $this->formulaTokens[$this->formulaPos] ?? null;
    +        if ($t === null) {
    +            throw new \RuntimeException('Unexpected end of formula');
    +        }
    +        $this->formulaPos++;
    +        return $t;
    +    }
    +
    +    // expr = term (('+' | '-') term)*
    +    private function parseAddSub(): float
    +    {
    +        $left = $this->parseMulDivMod();
    +        while (($t = $this->peekToken()) !== null && $t['type'] === 'op' && in_array($t['value'], ['+', '-'], true)) {
    +            $this->consumeToken();
    +            $right = $this->parseMulDivMod();
    +            $left = $t['value'] === '+' ? $left + $right : $left - $right;
    +        }
    +        return $left;
    +    }
    +
    +    // term = power (('*' | '/' | '%') power)*
    +    private function parseMulDivMod(): float
    +    {
    +        $left = $this->parsePower();
    +        while (($t = $this->peekToken()) !== null && $t['type'] === 'op' && in_array($t['value'], ['*', '/', '%'], true)) {
    +            $this->consumeToken();
    +            $right = $this->parsePower();
    +            if ($t['value'] === '*') {
    +                $left = $left * $right;
    +            } elseif ($t['value'] === '/') {
    +                $left = $right != 0 ? $left / $right : 0.0;
    +            } else {
    +                $left = $right != 0 ? fmod($left, $right) : 0.0;
    +            }
    +        }
    +        return $left;
    +    }
    +
    +    // power = unary ('^' unary)* — right-associative
    +    private function parsePower(): float
    +    {
    +        $base = $this->parseUnary();
    +        if (($t = $this->peekToken()) !== null && $t['type'] === 'op' && $t['value'] === '^') {
    +            $this->consumeToken();
    +            return pow($base, $this->parsePower());
    +        }
    +        return $base;
    +    }
    +
    +    // unary = '-' unary | primary
    +    private function parseUnary(): float
    +    {
    +        $t = $this->peekToken();
    +        if ($t !== null && $t['type'] === 'op' && $t['value'] === '-') {
    +            $this->consumeToken();
    +            return -$this->parseUnary();
    +        }
    +        return $this->parsePrimary();
    +    }
    +
    +    // primary = number | 'pi' ['()'] | function '(' expr ')' | '(' expr ')'
    +    private function parsePrimary(): float
    +    {
    +        $t = $this->consumeToken();
    +        if ($t['type'] === 'number') {
    +            return (float) $t['value'];
    +        }
    +        if ($t['type'] === 'name') {
    +            if ($t['value'] === 'pi') {
    +                // accept both bare "pi" and legacy "pi()"
    +                $next = $this->peekToken();
    +                if ($next !== null && $next['type'] === 'op' && $next['value'] === '(') {
    +                    $this->consumeToken();
    +                    $close = $this->consumeToken();
    +                    if ($close['type'] !== 'op' || $close['value'] !== ')') {
    +                        throw new \RuntimeException("Expected ')' after pi()");
    +                    }
    +                }
    +                return M_PI;
    +            }
    +            $fn = self::ALLOWED_FUNCTIONS[$t['value']] ?? null;
    +            if ($fn === null) {
    +                throw new \RuntimeException("Unknown function: {$t['value']}");
    +            }
    +            $open = $this->consumeToken();
    +            if ($open['type'] !== 'op' || $open['value'] !== '(') {
    +                throw new \RuntimeException("Expected '(' after {$t['value']}");
    +            }
    +            $arg = $this->parseAddSub();
    +            $close = $this->consumeToken();
    +            if ($close['type'] !== 'op' || $close['value'] !== ')') {
    +                throw new \RuntimeException("Expected ')' after function argument");
    +            }
    +            return (float) $fn($arg);
    +        }
    +        if ($t['type'] === 'op' && $t['value'] === '(') {
    +            $val = $this->parseAddSub();
    +            $close = $this->consumeToken();
    +            if ($close['type'] !== 'op' || $close['value'] !== ')') {
    +                throw new \RuntimeException("Expected ')'");
    +            }
    +            return $val;
    +        }
    +        throw new \RuntimeException("Unexpected token: {$t['type']} '{$t['value']}'");
    +    }
    +
         public function getCalcFormula(): ?string
         {
             return $this->calcFormula;
    

Vulnerability mechanics

Root cause

"The application uses PHP's eval() function to process user-supplied mathematical formulas without adequate sanitization, leading to arbitrary code execution or denial of service."

Attack vector

An attacker can exploit this vulnerability by creating or editing a Bazar form field that uses the CalcField.php calculator. By injecting a deeply nested recursive mathematical structure, an attacker can trigger a PCRE stack overflow, causing a denial of service [ref_id=2]. Alternatively, if the regular expression validation is bypassed, the attacker can inject arbitrary PHP code, such as calling the `system()` function, to achieve remote code execution [ref_id=2].

Affected code

The vulnerability resides in the `formatValuesBeforeSave` method of `tools/bazar/fields/CalcField.php`. This method previously used a complex recursive regular expression to validate user-defined formulas before passing them to the `eval()` function.

What the fix does

The patch replaces the vulnerable use of `eval()` with a custom parser for mathematical formulas. This new parser, implemented in `evaluateFormula` and its helper methods, tokenizes the input and evaluates it according to defined mathematical rules and a whitelist of allowed functions [patch_id=5244823]. This approach prevents the execution of arbitrary PHP code and mitigates the risk of stack overflows associated with complex regular expressions.

Preconditions

  • inputThe attacker must be able to input a mathematical formula into a Bazar form field that uses the CalcField.php calculator.

Generated on Jun 8, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

3

News mentions

0

No linked articles in our index yet.