Skip to content

Fix CVE-2022-4993 stop routing foreign text into the Locale::Maketext format - #159

Open
robrwo wants to merge 1 commit into
gshank:masterfrom
robrwo:CVE-2022-4993
Open

Fix CVE-2022-4993 stop routing foreign text into the Locale::Maketext format#159
robrwo wants to merge 1 commit into
gshank:masterfrom
robrwo:CVE-2022-4993

Conversation

@robrwo

@robrwo robrwo commented Aug 14, 2026

Copy link
Copy Markdown

The first argument to add_error is the Locale::Maketext FORMAT: bracket groups in it are compiled into method-dispatch code. Three kinds of text that FormHandler did not author reach that position, all of them carrying submitted request data:

  • _apply_actions traps warnings into $error_message (Validate.pm, the $SIG{WARN} handler). A warning survives a SUCCESSFUL action, so an ordinary numeric transform on a text field turns Argument "[sprintf,%2000000000d,0]" isn't numeric into the format -- Perl quotes the value verbatim, so the group is well formed and reaches CORE::sprintf with an attacker-chosen width (~GB allocation).
  • a type constraint's failure message. Moose renders the rejected value with Devel::PartialDump when it can load it, Type::Tiny always uses its own dumper, and both render a reference in bracket-and-comma form -- so on any field with apply => [ Str ], two same-named request parameters put [ "a", "b" ] in the format and maketext croaks, which add_error re-dies: an unhandled 500 with no payload at all.
  • exceptions from a coercion or transform.
  • a date parser's error message. Field/Date.pm passes $strp->errmsg || $@ -- DateTime::Format::Strptime's own text -- straight into the format position. DTFS 1.80 answers a rejected value with the fixed string "Your datetime does not match your pattern.", so there is no reachable payload through it today; that is a property of the current version of a separate distribution rather than of this code, and the || $@ fallback is a second channel that was not exercised. Escaped for the same reason as the others.

Escape the bracket-notation metacharacters in all four before they are used as a format. Tilde is Locale::Maketext's escape, and text with no brackets is returned unchanged, so lexicon lookups and translated type-constraint messages are byte-identical to before.

Escape the bracket-notation metacharacters in all four before they are used as a format. Tilde is Locale::Maketext's escape, and text with no brackets is returned unchanged, so lexicon lookups and translated type-constraint messages are byte-identical to before.

Also: add_error derefs an arrayref first argument into (template, @Args). That spelling is the same list-or-arrayref convenience idiom as add_element_class and friends; it is not documented for add_error, and an instrumented run of the distribution's own suite (150 files, 1491 tests) never reaches the branch. What does reach it is request data -- $field->add_error($field->value) where the request parser folded a duplicate parameter into an arrayref puts submitted text in element 0. Since no message the library raises arrives in that shape, treat an arrayref argument as a value: keep the deref, render element 0 literally. A caller who wants a compiled template passes it as a plain list, $field->add_error($template, @args), which is the documented spelling and is unchanged.

One case cannot be fixed here: an application that concatenates the value into its own message, add_error("The value '" . $field->value . "' is not allowed"), is indistinguishable from a legitimate template, so the add_error POD now documents the hazard and the inert-argument idiom.

Behaviour trade-offs -- the only output changes outside the attack cases:

  • a custom type constraint whose own message block uses bracket notation (message { 'Try [quant,1,thing]' }) now renders that literally. Such a block receives the rejected value and can interpolate it, so escaping it is the safe default; a maintainer who would rather keep those compiled can exempt the has_message branch specifically.
  • an application calling the undocumented arrayref spelling with a template that uses bracket notation, add_error([ 'Try [quant,_1,thing]', 3 ]), now renders it literally; the list spelling of the same call still compiles.

FormHandler's own message templates, and application templates passed to add_error together with their arguments, are unaffected.

Verified against the 0.40068 test suite: 150 files, 1491 tests, PASS both before and after. A before/after table of rendered error messages (maxlength, minlength, required, invalid select value, integer range, duplicate-parameter arrays, application template with arguments, plain and bracketed type messages, plain and bracketed warnings and exceptions) is byte-identical except the lines above.

…t format

The first argument to add_error is the Locale::Maketext FORMAT: bracket groups
in it are compiled into method-dispatch code. Three kinds of text that
FormHandler did not author reach that position, all of them carrying submitted
request data:

  * _apply_actions traps warnings into $error_message (Validate.pm, the
    $SIG{__WARN__} handler). A warning survives a SUCCESSFUL action, so an
    ordinary numeric transform on a text field turns
    `Argument "[sprintf,%2000000000d,0]" isn't numeric` into the format --
    Perl quotes the value verbatim, so the group is well formed and reaches
    CORE::sprintf with an attacker-chosen width (~GB allocation).
  * a type constraint's failure message. Moose renders the rejected value with
    Devel::PartialDump when it can load it, Type::Tiny always uses its own
    dumper, and both render a reference in bracket-and-comma form -- so on any
    field with `apply => [ Str ]`, two same-named request parameters put
    `[ "a", "b" ]` in the format and maketext croaks, which add_error re-dies:
    an unhandled 500 with no payload at all.
  * exceptions from a coercion or transform.
  * a date parser's error message. Field/Date.pm passes
    `$strp->errmsg || $@` -- DateTime::Format::Strptime's own text -- straight
    into the format position. DTFS 1.80 answers a rejected value with the
    fixed string "Your datetime does not match your pattern.", so there is no
    reachable payload through it today; that is a property of the current
    version of a separate distribution rather than of this code, and the
    `|| $@` fallback is a second channel that was not exercised. Escaped for
    the same reason as the others.

Escape the bracket-notation metacharacters in all four before they are used
as a format. Tilde is Locale::Maketext's escape, and text with no brackets is
returned unchanged, so lexicon lookups and translated type-constraint messages
are byte-identical to before.

Escape the bracket-notation metacharacters in all four before they are used
as a format. Tilde is Locale::Maketext's escape, and text with no brackets is
returned unchanged, so lexicon lookups and translated type-constraint messages
are byte-identical to before.

Also: add_error derefs an arrayref first argument into (template, @Args). That
spelling is the same list-or-arrayref convenience idiom as add_element_class
and friends; it is not documented for add_error, and an instrumented run of the
distribution's own suite (150 files, 1491 tests) never reaches the branch. What
does reach it is request data -- `$field->add_error($field->value)` where the
request parser folded a duplicate parameter into an arrayref puts submitted
text in element 0. Since no message the library raises arrives in that shape,
treat an arrayref argument as a value: keep the deref, render element 0
literally. A caller who wants a compiled template passes it as a plain list,
`$field->add_error($template, @Args)`, which is the documented spelling and is
unchanged.

One case cannot be fixed here: an application that concatenates the value into
its own message, `add_error("The value '" . $field->value . "' is not
allowed")`, is indistinguishable from a legitimate template, so the add_error
POD now documents the hazard and the inert-argument idiom.

Behaviour trade-offs -- the only output changes outside the attack cases:

  * a custom type constraint whose own message block uses bracket notation
    (message { 'Try [quant,1,thing]' }) now renders that literally. Such a
    block receives the rejected value and can interpolate it, so escaping it
    is the safe default; a maintainer who would rather keep those compiled can
    exempt the has_message branch specifically.
  * an application calling the undocumented arrayref spelling with a template
    that uses bracket notation, add_error([ 'Try [quant,_1,thing]', 3 ]), now
    renders it literally; the list spelling of the same call still compiles.

FormHandler's own message templates, and application templates passed to
add_error together with their arguments, are unaffected.

Verified against the 0.40068 test suite: 150 files, 1491 tests, PASS both
before and after. A before/after table of rendered error messages
(maxlength, minlength, required, invalid select value, integer range,
duplicate-parameter arrays, application template with arguments, plain and
bracketed type messages, plain and bracketed warnings and exceptions) is
byte-identical except the lines above.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Signed-off-by: Robert Rothenberg <rrwo@cpansec.org>
@abraxxa
abraxxa requested review from abraxxa and zby August 14, 2026 08:57
@abraxxa abraxxa self-assigned this Aug 14, 2026
@robrwo

robrwo commented Aug 14, 2026

Copy link
Copy Markdown
Author

@abraxxa @zby For future reference, who is maintaining this module, and who should the security contact be?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants