Skip to content

Refuse blessed objects and code in YAML request bodies - #1838

Merged
cromedome merged 1 commit into
mainfrom
bigpresh/yaml-loadblessed
Sep 17, 2026
Merged

cromedome merged 1 commit into
mainfrom
bigpresh/yaml-loadblessed

Conversation

@bigpresh

Copy link
Copy Markdown
Member

What

Dancer2::Serializer::YAML::deserialize handed request bodies straight to
YAML::Load. A body tagged !!perl/hash:Some::Class therefore instantiated an
arbitrary blessed object — the entry point for DESTROY/AUTOLOAD/overload
gadget chains — and !!perl/code could ask for a string eval.

deserialize now sets $YAML::LoadBlessed = 0 and $YAML::LoadCode = 0 itself
(localised) before loading, rather than relying on YAML.pm's ambient defaults,
and the minimum YAML is raised to 1.30.

Why do it in our code and bump the dependency

  • $YAML::LoadBlessed only exists from YAML 1.25; below that, setting it is a
    silent no-op. So the explicit setting needs a version floor to mean anything.
  • Setting it ourselves (rather than leaning on 1.30's safe default) keeps the
    guarantee independent of which YAML the user resolved, and holds even when the
    surrounding process has set the variable to something hostile — which is not
    hypothetical: Dancer2::Session::YAML sets LoadBlessed = 1 for its own load.
  • The floor is 1.30 rather than 1.25 because 1.26 fixed a parsing regression
    in 1.25 and 1.28 carries an upstream security fix in the same load path
    ("only enable loading globs when $LoadCode is set") that is behavioural and
    cannot be reproduced from our side. 1.30 only adds the changed default, which
    we override either way, but it is from Jan 2020 and costs nothing. The new
    floor supersedes the old 0.86 minimum and the 1.16 exclusion, now dropped.

Reachability

Not limited to apps that set serializer: YAML. Serializer::Mutable maps both
text/x-yaml and text/html to this class, so a request body under a
common content type reaches YAML::Load with no YAML configuration anywhere.
And Request deserializes the body eagerly, before any route runs, so a
DESTROY-based gadget does not even need the route to touch the parsed data.

The !!perl/code direct-eval path was gated by $YAML::LoadCode, which defaults
off and which Dancer2 never enabled, so it was not reachable by default; it is
closed here regardless as belt-and-braces.

Tests

t/integration/serializer/yaml.t sets a hostile ambient $YAML::LoadBlessed = 1
before each assertion, so it exercises the guard rather than passing on the safe
default a recent YAML would give anyway, and includes a control proving the
payload really does bless without it. The assertions test only the security
property ("no attacker-named object comes back"), not the exact shape of what
does — that shape is YAML-version-dependent (older YAML strips the tag and
returns a plain hash; newer YAML rejects the document), and pinning either one
is what made an earlier draft pass locally but fail on CI.

Full suite passes locally (208 files, 1851 tests) on YAML 1.30.

Related

The version split above surfaced #1837 (Role::Serializer's error handler is
not class-safe): on newer YAML a rejected blessed body throws, and a
class-method deserialize call routes that through the unguarded log_cb. The
framework always calls deserialize on an instance, so it is unaffected in
normal use, and the test now does the same — but #1837 is worth fixing on its
own.

Dancer2::Serializer::YAML handed request bodies straight to YAML::Load.
A body tagged !!perl/hash:Some::Class therefore instantiated an arbitrary
blessed object -- the entry point for DESTROY/AUTOLOAD gadget chains --
and this was reachable without anyone configuring YAML explicitly, since
Serializer::Mutable maps both text/x-yaml and text/html to this class.

deserialize now sets $YAML::LoadBlessed and $YAML::LoadCode to 0 itself,
localised, rather than relying on YAML.pm's defaults. Doing it here keeps
the guarantee independent of which YAML.pm the user resolved, and holds
even when the surrounding process has set those variables to something
hostile -- which is not hypothetical: Dancer2::Session::YAML sets
LoadBlessed to 1 for its own load.

That explicit setting only takes effect from YAML 1.25, where the
variable was introduced; below it, setting it is a silent no-op. The
floor moves to 1.30 rather than 1.25 because 1.26 fixed a parsing
regression in 1.25, and 1.28 carries an upstream security fix in the
same load path ("only enable loading globs when $LoadCode is set") which
is behavioural and so cannot be reproduced from our side. 1.30 itself
adds only the changed default, which we override either way, but it is
from January 2020 and costs nothing. The new floor also supersedes the
previous 0.86 minimum and the 1.16 exclusion, which are dropped.

The tests set a hostile ambient LoadBlessed before each assertion, so
they exercise the guard rather than passing on the safe default that
YAML 1.30 would give them anyway, and include a control asserting the
payload really does bless without it.
@bigpresh
bigpresh requested a review from cromedome September 16, 2026 23:52

@cromedome cromedome left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch! 👍 Approved.

@veryrusty veryrusty left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 secure be default is the way to go. Thanks @bigpresh !

Note: I pondered the potential to break existing code that relied on insecure deserialisation; but settled on that its trivial to write your own serialiser if you want to do unsafe things.

@cromedome
cromedome merged commit c5b72a1 into main Sep 17, 2026
18 checks passed
@cromedome
cromedome deleted the bigpresh/yaml-loadblessed branch September 17, 2026 02:12
cromedome added a commit that referenced this pull request Sep 19, 2026
    [ SECURITY ]
    * PR #1838: Serializer::YAML refuses blessed objects and code in
      request bodies; raise min YAML version to 1.30 (David Precious)
    * PR #1839: Serializer::YAML also zeroes $YAML::UseCode, which
      otherwise reopens the code eval (David Precious)

    [ BUG FIXES ]
    * GH #1840: Specify min version of CLI::Osprey needed (Jason A. Crome)

    [ ENHANCEMENTS ]
    * None

    [ DOCUMENTATION ]
    * None

    [ DEPRECATED ]
    * None

    [ MISC ]
    * None
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.

3 participants