--- a/includes/vb5/template/runtime.php
+++ b/includes/vb5/template/runtime.php
@@ -400,62 +400,207 @@
 
 	public static function runMaths($str)
 	{
-		//this would usually be dangerous, but none of the units make sense
-		//in a math string anyway.  Note that there is ambiguty between the '%'
-		//unit and the modulo operator.  We don't allow the latter anyway
-		//(though we do allow bitwise operations !?)
-		$units_found = null;
+		if (!is_scalar($str))
+		{
+			return "/* Invalid math expression */";
+		}
+
+		$str = (string) $str;
+		$units_found = array();
 		foreach (self::$units AS $unit)
 		{
-			if (strpos($str, $unit))
+			if (strpos($str, $unit) !== false)
 			{
 				$units_found[] = $unit;
 			}
 		}
 
-		//mixed units.
 		if (count($units_found) > 1)
 		{
 			return "/* ~~cannot perform math on mixed units ~~ found (" .
-			implode(",", $units_found) . ") in $str */";
+				implode(",", $units_found) . ") in $str */";
+		}
+
+		$unit = count($units_found) == 1 ? $units_found[0] : '';
+		if ($unit !== '')
+		{
+			$str = str_replace($unit, '', $str);
+		}
+
+		// Never pass template data to PHP's evaluator. The old character filter
+		// retained enough PHP syntax to construct and invoke arbitrary functions.
+		// Parse the small arithmetic grammar that templates actually require.
+		$value = self::evaluateMathExpression($str);
+		if ($value === false)
+		{
+			if (!headers_sent())
+			{
+				header("HTTP/1.1 200 OK");
+			}
+			return "/* Invalid math expression */";
+		}
+
+		return $value . $unit;
+	}
+
+	private static function evaluateMathExpression($str)
+	{
+		if (!is_scalar($str))
+		{
+			return false;
+		}
+
+		$str = (string) $str;
+		if (strlen($str) > 1024 OR !preg_match('~\A[\s0-9+\-*/().]+\z~D', $str))
+		{
+			return false;
 		}
 
-		$str = preg_replace('#([^+\-*=/\(\)\d\^<>&|\.]*)#', '', $str);
+		$offset = 0;
+		$operations = 0;
+		$value = self::parseMathAddSubtract($str, $offset, 0, $operations);
+		self::skipMathWhitespace($str, $offset);
+		if ($value === false OR $offset !== strlen($str) OR !is_finite($value))
+		{
+			return false;
+		}
+
+		// Match eval()'s normal string conversion without permitting non-finite
+		// values or executable syntax.
+		return (string) (0 + $value);
+	}
 
-		if (empty($str))
+	private static function parseMathAddSubtract($str, &$offset, $depth, &$operations)
+	{
+		$value = self::parseMathMultiplyDivide($str, $offset, $depth, $operations);
+		if ($value === false)
 		{
-			$str = '0';
+			return false;
 		}
-		else
+
+		while (true)
 		{
-			//hack: if the math string is invalid we can get a php parse error here.
-			//a bad expression or even a bad variable value (blank instead of a number) can
-			//cause this to occur.  This fails quietly, but also sets the status code to 500
-			//(but, due to a bug in php only if display_errors is *off* -- if display errors
-			//is on, then it will work just fine only $str below will not be set.
-			//
-			//This can result is say an almost correct css file being ignored by the browser
-			//for reasons that aren't clear (and goes away if you turn error reporting on).
-			//We can check to see if eval hit a parse error and, if so, we'll attempt to
-			//clear the 500 status (this does more harm then good) and send an error
-			//to the file.  Since math is mostly used in css, we'll provide error text
-			//that works best with that.
-			$status = @eval("\$str = $str;");
-			if ($status === false)
+			self::skipMathWhitespace($str, $offset);
+			$operator = isset($str[$offset]) ? $str[$offset] : '';
+			if ($operator !== '+' AND $operator !== '-')
 			{
-				if (!headers_sent())
-				{
-					header("HTTP/1.1 200 OK");
-				}
-				return "/* Invalid math expression */";
+				return $value;
 			}
+			$offset++;
+			if (++$operations > 256)
+			{
+				return false;
+			}
+			$right = self::parseMathMultiplyDivide($str, $offset, $depth, $operations);
+			if ($right === false)
+			{
+				return false;
+			}
+			$value = ($operator === '+') ? $value + $right : $value - $right;
+			if (!is_finite($value))
+			{
+				return false;
+			}
+		}
+	}
+
+	private static function parseMathMultiplyDivide($str, &$offset, $depth, &$operations)
+	{
+		$value = self::parseMathUnary($str, $offset, $depth, $operations);
+		if ($value === false)
+		{
+			return false;
+		}
 
-			if (count($units_found) == 1)
+		while (true)
+		{
+			self::skipMathWhitespace($str, $offset);
+			$operator = isset($str[$offset]) ? $str[$offset] : '';
+			if ($operator !== '*' AND $operator !== '/')
+			{
+				return $value;
+			}
+			$offset++;
+			if (++$operations > 256)
+			{
+				return false;
+			}
+			$right = self::parseMathUnary($str, $offset, $depth, $operations);
+			if ($right === false OR ($operator === '/' AND $right == 0))
 			{
-				$str = $str . $units_found[0];
+				return false;
 			}
+			$value = ($operator === '*') ? $value * $right : $value / $right;
+			if (!is_finite($value))
+			{
+				return false;
+			}
+		}
+	}
+
+	private static function parseMathUnary($str, &$offset, $depth, &$operations)
+	{
+		self::skipMathWhitespace($str, $offset);
+		$sign = 1;
+		while (isset($str[$offset]) AND ($str[$offset] === '+' OR $str[$offset] === '-'))
+		{
+			if ($str[$offset++] === '-')
+			{
+				$sign *= -1;
+			}
+			if (++$operations > 256)
+			{
+				return false;
+			}
+			self::skipMathWhitespace($str, $offset);
+		}
+
+		$value = self::parseMathPrimary($str, $offset, $depth, $operations);
+		return $value === false ? false : $sign * $value;
+	}
+
+	private static function parseMathPrimary($str, &$offset, $depth, &$operations)
+	{
+		self::skipMathWhitespace($str, $offset);
+		if (!isset($str[$offset]))
+		{
+			return false;
+		}
+
+		if ($str[$offset] === '(')
+		{
+			if ($depth >= 32)
+			{
+				return false;
+			}
+			$offset++;
+			$value = self::parseMathAddSubtract($str, $offset, $depth + 1, $operations);
+			self::skipMathWhitespace($str, $offset);
+			if ($value === false OR !isset($str[$offset]) OR $str[$offset] !== ')')
+			{
+				return false;
+			}
+			$offset++;
+			return $value;
+		}
+
+		$remainder = substr($str, $offset);
+		if (!preg_match('~\A(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)~', $remainder, $match))
+		{
+			return false;
+		}
+		$offset += strlen($match[0]);
+		$value = (float) $match[0];
+		return is_finite($value) ? $value : false;
+	}
+
+	private static function skipMathWhitespace($str, &$offset)
+	{
+		$length = strlen($str);
+		while ($offset < $length AND strpos(" \t\r\n", $str[$offset]) !== false)
+		{
+			$offset++;
 		}
-		return $str;
 	}
 
 	public static function linkBuild($type, $info = array(), $extra = array(), $primaryid = null, $primarytitle = null)
--- a/core/vb/template/runtime.php
+++ b/core/vb/template/runtime.php
@@ -359,62 +359,207 @@
 
 	public static function runMaths($str)
 	{
-		//this would usually be dangerous, but none of the units make sense
-		//in a math string anyway.  Note that there is ambiguty between the '%'
-		//unit and the modulo operator.  We don't allow the latter anyway
-		//(though we do allow bitwise operations !?)
-		$units_found = null;
+		if (!is_scalar($str))
+		{
+			return "/* Invalid math expression */";
+		}
+
+		$str = (string) $str;
+		$units_found = array();
 		foreach (self::$units AS $unit)
 		{
-			if (strpos($str, $unit))
+			if (strpos($str, $unit) !== false)
 			{
 				$units_found[] = $unit;
 			}
 		}
 
-		//mixed units.
 		if (count($units_found) > 1)
 		{
 			return "/* ~~cannot perform math on mixed units ~~ found (" .
 				implode(",", $units_found) . ") in $str */";
 		}
 
-		$str = preg_replace('#([^+\-*=/\(\)\d\^<>&|\.]*)#', '', $str);
+		$unit = count($units_found) == 1 ? $units_found[0] : '';
+		if ($unit !== '')
+		{
+			$str = str_replace($unit, '', $str);
+		}
+
+		// Never pass template data to PHP's evaluator. The old character filter
+		// retained enough PHP syntax to construct and invoke arbitrary functions.
+		// Parse the small arithmetic grammar that templates actually require.
+		$value = self::evaluateMathExpression($str);
+		if ($value === false)
+		{
+			if (!headers_sent())
+			{
+				header("HTTP/1.1 200 OK");
+			}
+			return "/* Invalid math expression */";
+		}
+
+		return $value . $unit;
+	}
+
+	private static function evaluateMathExpression($str)
+	{
+		if (!is_scalar($str))
+		{
+			return false;
+		}
+
+		$str = (string) $str;
+		if (strlen($str) > 1024 OR !preg_match('~\A[\s0-9+\-*/().]+\z~D', $str))
+		{
+			return false;
+		}
+
+		$offset = 0;
+		$operations = 0;
+		$value = self::parseMathAddSubtract($str, $offset, 0, $operations);
+		self::skipMathWhitespace($str, $offset);
+		if ($value === false OR $offset !== strlen($str) OR !is_finite($value))
+		{
+			return false;
+		}
+
+		// Match eval()'s normal string conversion without permitting non-finite
+		// values or executable syntax.
+		return (string) (0 + $value);
+	}
 
-		if (empty($str))
+	private static function parseMathAddSubtract($str, &$offset, $depth, &$operations)
+	{
+		$value = self::parseMathMultiplyDivide($str, $offset, $depth, $operations);
+		if ($value === false)
 		{
-			$str = '0';
+			return false;
 		}
-		else
+
+		while (true)
 		{
-			//hack: if the math string is invalid we can get a php parse error here.
-			//a bad expression or even a bad variable value (blank instead of a number) can
-			//cause this to occur.  This fails quietly, but also sets the status code to 500
-			//(but, due to a bug in php only if display_errors is *off* -- if display errors
-			//is on, then it will work just fine only $str below will not be set.
-			//
-			//This can result is say an almost correct css file being ignored by the browser
-			//for reasons that aren't clear (and goes away if you turn error reporting on).
-			//We can check to see if eval hit a parse error and, if so, we'll attempt to
-			//clear the 500 status (this does more harm then good) and send an error
-			//to the file.  Since math is mostly used in css, we'll provide error text
-			//that works best with that.
-			$status = @eval("\$str = $str;");
-			if ($status === false)
+			self::skipMathWhitespace($str, $offset);
+			$operator = isset($str[$offset]) ? $str[$offset] : '';
+			if ($operator !== '+' AND $operator !== '-')
 			{
-				if (!headers_sent())
-				{
-					header("HTTP/1.1 200 OK");
-				}
-				return "/* Invalid math expression */";
+				return $value;
 			}
+			$offset++;
+			if (++$operations > 256)
+			{
+				return false;
+			}
+			$right = self::parseMathMultiplyDivide($str, $offset, $depth, $operations);
+			if ($right === false)
+			{
+				return false;
+			}
+			$value = ($operator === '+') ? $value + $right : $value - $right;
+			if (!is_finite($value))
+			{
+				return false;
+			}
+		}
+	}
+
+	private static function parseMathMultiplyDivide($str, &$offset, $depth, &$operations)
+	{
+		$value = self::parseMathUnary($str, $offset, $depth, $operations);
+		if ($value === false)
+		{
+			return false;
+		}
 
-			if (count($units_found) == 1)
+		while (true)
+		{
+			self::skipMathWhitespace($str, $offset);
+			$operator = isset($str[$offset]) ? $str[$offset] : '';
+			if ($operator !== '*' AND $operator !== '/')
+			{
+				return $value;
+			}
+			$offset++;
+			if (++$operations > 256)
+			{
+				return false;
+			}
+			$right = self::parseMathUnary($str, $offset, $depth, $operations);
+			if ($right === false OR ($operator === '/' AND $right == 0))
 			{
-				$str = $str.$units_found[0];
+				return false;
 			}
+			$value = ($operator === '*') ? $value * $right : $value / $right;
+			if (!is_finite($value))
+			{
+				return false;
+			}
+		}
+	}
+
+	private static function parseMathUnary($str, &$offset, $depth, &$operations)
+	{
+		self::skipMathWhitespace($str, $offset);
+		$sign = 1;
+		while (isset($str[$offset]) AND ($str[$offset] === '+' OR $str[$offset] === '-'))
+		{
+			if ($str[$offset++] === '-')
+			{
+				$sign *= -1;
+			}
+			if (++$operations > 256)
+			{
+				return false;
+			}
+			self::skipMathWhitespace($str, $offset);
+		}
+
+		$value = self::parseMathPrimary($str, $offset, $depth, $operations);
+		return $value === false ? false : $sign * $value;
+	}
+
+	private static function parseMathPrimary($str, &$offset, $depth, &$operations)
+	{
+		self::skipMathWhitespace($str, $offset);
+		if (!isset($str[$offset]))
+		{
+			return false;
+		}
+
+		if ($str[$offset] === '(')
+		{
+			if ($depth >= 32)
+			{
+				return false;
+			}
+			$offset++;
+			$value = self::parseMathAddSubtract($str, $offset, $depth + 1, $operations);
+			self::skipMathWhitespace($str, $offset);
+			if ($value === false OR !isset($str[$offset]) OR $str[$offset] !== ')')
+			{
+				return false;
+			}
+			$offset++;
+			return $value;
+		}
+
+		$remainder = substr($str, $offset);
+		if (!preg_match('~\A(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)~', $remainder, $match))
+		{
+			return false;
+		}
+		$offset += strlen($match[0]);
+		$value = (float) $match[0];
+		return is_finite($value) ? $value : false;
+	}
+
+	private static function skipMathWhitespace($str, &$offset)
+	{
+		$length = strlen($str);
+		while ($offset < $length AND strpos(" \t\r\n", $str[$offset]) !== false)
+		{
+			$offset++;
 		}
-		return $str;
 	}
 
 	public static function linkBuild($type, $info = array(), $extra = array(), $primaryid = null, $primarytitle = null)
