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
2Patches
1dd2bd8fb099dfix(bazar): GHSA-px5m-h76g-p7p8
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
3News mentions
0No linked articles in our index yet.