getgrav/grav · error · RuntimeError

Twig |reduce("{arrow}") is not allowed.

Error message

Twig |reduce("{arrow}") is not allowed.

What it means

Grav's override of Twig's |reduce applies the same guard as |filter and |map: the arrow must be a \Closure or a string that is not flagged by Utils::isDangerousFunction(), otherwise a RuntimeError is thrown before delegation. A string like 'system' passed to reduce would be invoked as system($accumulator, $value) outside the sandbox, hence the refusal. Note that in this codebase reduceFunc currently delegates to twig_array_map rather than a reduce implementation — if |reduce silently behaves like |map after passing the guard, check your Grav version against upstream.

Source

Thrown at system/src/Grav/Common/Twig/Extension/GravExtension.php:2111

        if ($array === null) {
            $array = [];
        }

        return twig_array_map($env, $array, $arrow);
    }

    /**
     * @param Environment $env
     * @param array $array
     * @param callable|string $arrow
     * @return array|CallbackFilterIterator
     * @throws RuntimeError
     */
    function reduceFunc(Environment $env, $array, $arrow)
    {
        if (!$arrow instanceof \Closure && !is_string($arrow) || Utils::isDangerousFunction($arrow)) {
            throw new RuntimeError('Twig |reduce("' . $arrow . '") is not allowed.');
        }

        if ($array === null) {
            $array = [];
        }

        return twig_array_map($env, $array, $arrow);
    }

    /**
     * Hardened `find` filter. Twig core only rejects a dangerous string callable
     * (e.g. `find('system')`, invoked as `system($v, $k)`) when the template is
     * sandboxed. Editor-authorable strings rendered OUTSIDE the sandbox — such as
     * the Email plugin's form action params — reached that unguarded call and gave
     * a page editor RCE (GHSA-xx48-97m4-h7qm). Apply the same dangerous-arrow guard
     * used by filter/map/reduce, regardless of sandbox state; a real arrow closure
     * still passes.
     *

View on GitHub (pinned to 6040efed04)

Solutions

  1. Rewrite the reduction as an arrow function: {{ items|reduce((carry, v) => carry + v.price, 0) }}
  2. Wrap class methods instead of array callables: {{ items|reduce((carry, v) => MyClass::combine(carry, v), '') }}
  3. Treat denylisted names in templates as a security signal and audit the template's origin
  4. Never pass an uninitialized (null) arrow to |reduce

Example fix

{# before: string callable #}
{{ items|reduce('system') }}

{# after: arrow function Closure #}
{{ items|reduce((carry, v) => carry + v.price, 0) }}
Defensive patterns

Strategy: validation

Validate before calling

// guard the arrow before render (mirrors reduceFunc's check)
$ok = $arrow instanceof \Closure || (is_string($arrow) && !Utils::isDangerousFunction($arrow));
if (!$ok) { $arrow = fn($carry, $v) => $carry; // safe default or fail fast
}

Type guard

function isSafeTwigArrow(mixed $arrow): bool
{
    return $arrow instanceof \Closure || (is_string($arrow) && !\Grav\Common\Utils::isDangerousFunction($arrow));
}

Try / catch

use Twig\Error\RuntimeError;
try { echo $twig->render($template, $data); }
catch (RuntimeError $e) { log_template_error($e); // audit template provenance
}

Prevention

When it happens

Trigger: {{ items|reduce('system') }} or any denylisted function name; passing a null arrow or an array callable (['MyClass', 'combine']) which is neither Closure nor string; legacy templates written against Twig versions where string callables were accepted unchecked.

Common situations: Migrating pre-hardening themes; aggregation snippets (summing, concatenating) written with string callables; injected templates tripping the RCE guard.

Related errors


AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17). Data as JSON: /api/errors/d41d07bc9680a941. Report an issue: GitHub.