Refuse blessed objects and code in YAML request bodies - #1838
Merged
Merged
Conversation
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.
cromedome
approved these changes
Sep 17, 2026
cromedome
left a comment
Contributor
There was a problem hiding this comment.
Nice catch! 👍 Approved.
veryrusty
approved these changes
Sep 17, 2026
veryrusty
left a comment
Member
There was a problem hiding this comment.
👍 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.
This was referenced Sep 17, 2026
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Dancer2::Serializer::YAML::deserializehanded request bodies straight toYAML::Load. A body tagged!!perl/hash:Some::Classtherefore instantiated anarbitrary blessed object — the entry point for
DESTROY/AUTOLOAD/overloadgadget chains — and
!!perl/codecould ask for a stringeval.deserializenow sets$YAML::LoadBlessed = 0and$YAML::LoadCode = 0itself(localised) before loading, rather than relying on YAML.pm's ambient defaults,
and the minimum
YAMLis raised to 1.30.Why do it in our code and bump the dependency
$YAML::LoadBlessedonly exists from YAML 1.25; below that, setting it is asilent no-op. So the explicit setting needs a version floor to mean anything.
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::YAMLsetsLoadBlessed = 1for its own load.in 1.25 and 1.28 carries an upstream security fix in the same load path
("only enable loading globs when
$LoadCodeis set") that is behavioural andcannot 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.86minimum and the1.16exclusion, now dropped.Reachability
Not limited to apps that set
serializer: YAML.Serializer::Mutablemaps bothtext/x-yamlandtext/htmlto this class, so a request body under acommon content type reaches
YAML::Loadwith no YAML configuration anywhere.And
Requestdeserializes the body eagerly, before any route runs, so aDESTROY-based gadget does not even need the route to touch the parsed data.The
!!perl/codedirect-eval path was gated by$YAML::LoadCode, which defaultsoff 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.tsets a hostile ambient$YAML::LoadBlessed = 1before 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 isnot class-safe): on newer YAML a rejected blessed body throws, and a
class-method
deserializecall routes that through the unguardedlog_cb. Theframework always calls
deserializeon an instance, so it is unaffected innormal use, and the test now does the same — but #1837 is worth fixing on its
own.