kestra-io/kestra · error · PebbleException

RegexTimeoutException: e.getMessage()

Error message

RegexTimeoutException: e.getMessage()

What it means

Thrown by the 'replace' Pebble filter when `regexp=true` is set and one of the find patterns in `replace_pairs` triggers catastrophic backtracking, raising `RegexUtils.RegexTimeoutException`. Kestra enforces a regex execution timeout to prevent ReDoS; exceeding it aborts the substitution.

Source

Thrown at core/src/main/java/io/kestra/core/runners/pebble/filters/ReplaceFilter.java:57

                null,
                MessageFormat.format("The argument ''{0}'' is required.", ARGUMENT_PAIRS), lineNumber,
                self.getName()
            );
        }

        final boolean regexp = args.containsKey(ARGUMENT_REGEXP) ? (Boolean) args.get(ARGUMENT_REGEXP) : false;
        Map<?, ?> replacePair = (Map<?, ?>) args.get(ARGUMENT_PAIRS);

        try {
            if (input instanceof Map) {
                return processMap((Map<String, Object>) input, replacePair, regexp);
            } else if (input instanceof List) {
                return processList((List<Object>) input, replacePair, regexp);
            } else {
                return processString(input.toString(), replacePair, regexp);
            }
        } catch (RegexUtils.RegexTimeoutException e) {
            throw new PebbleException(e, e.getMessage(), lineNumber, self.getName());
        }
    }

    @SuppressWarnings("unchecked")
    private Object processMap(Map<String, Object> inputMap, Map<?, ?> replacePair, boolean regexp) {
        Map<String, Object> resultMap = new HashMap<>();
        for (Map.Entry<String, Object> entry : inputMap.entrySet()) {
            Object value = entry.getValue();
            if (value instanceof String stringValue) {
                resultMap.put(entry.getKey(), processString(stringValue, replacePair, regexp));
            } else if (value instanceof Map) {
                resultMap.put(entry.getKey(), processMap((Map<String, Object>) value, replacePair, regexp));
            } else if (value instanceof List<?>) {
                resultMap.put(entry.getKey(), processList((List<Object>) value, replacePair, regexp));
            } else {
                resultMap.put(entry.getKey(), processString(value.toString(), replacePair, regexp));
            }
        }

View on GitHub (pinned to 823fada927)

Solutions

  1. If you do not need regex semantics, set `regexp=false` (the default) so keys are treated as literals.
  2. Rewrite the vulnerable pattern to a linear-time equivalent.
  3. Pre-truncate long input before substitution.
  4. Audit each key in `replace_pairs` for nested unbounded quantifiers.

Example fix

# before
{{ body | replace(replace_pairs={"(a+)+b":"x"}, regexp=true) }}
# after - literal match (no regex)
{{ body | replace(replace_pairs={"ab":"x"}) }}
Defensive patterns

Strategy: validation

Validate before calling

# Prefer literal mode; only enable regexp with audited patterns and short inputs:
{% set safe = (body ?? "") | slice(0, 10000) %}
{{ safe | replace(replace_pairs={"a":"b"}) }}  {# regexp defaults to false #}

Prevention

When it happens

Trigger: A find-pair key that is a vulnerable regex (e.g. `(a+)+`) applied to a long input; multiple regex pairs where any one backtracks explosively; user-controlled input flowing into the find key.

Common situations: Enabling `regexp=true` with patterns copy-pasted from elsewhere; large log/HTTP bodies processed with greedy nested quantifiers; not realizing keys are interpreted as regexes.

Understand the failure class

Related errors


AI-assisted analysis of kestra-io/kestra@823fada927 (2026-08-14). Data as JSON: /api/errors/4c8bcba702934648. Report an issue: GitHub.