Fix CVE-2026-8450: send_file() honoured 2-arg open() shell-magic#89
Merged
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #89 +/- ##
==========================================
+ Coverage 8.57% 12.16% +3.58%
==========================================
Files 1 1
Lines 338 337 -1
Branches 86 88 +2
==========================================
+ Hits 29 41 +12
+ Misses 304 287 -17
- Partials 5 9 +4 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
robrwo
approved these changes
May 16, 2026
HTTP::Daemon::ClientConn::send_file() used the 2-arg form
open(FILE, $file), which interprets shell-magic prefixes in the
path argument: '| cmd' (write pipe -- RCE), 'cmd |' (read pipe --
RCE plus response-body exfiltration via the sysread / print loop
below), '> path' (write-truncate -- arbitrary file write), and
'>> path', '+< path', '<&fd', and leading-whitespace variants of
the above.
Any HTTP::Daemon-based application that passed attacker-influenced
bytes to send_file($string) -- for example, a download endpoint
that derived the filename from a query parameter -- granted command
execution and/or arbitrary file write at the daemon's UID.
Switch to 3-arg open(my $fh, '<', $file): the explicit '<' mode
makes the path argument a literal filename, so every magic shape
above is opened (and fails, returning undef) as an ordinary file by
that exact name. The localized typeglob is no longer needed and is
replaced with a lexical filehandle.
Two collateral hardening changes ride along:
- binmode() failure now closes the handle and returns undef,
rather than streaming the file with a wrong PerlIO layer.
- send_file() returns '0E0' (true zero) on a successful zero-byte
transfer so callers using "send_file or die" can distinguish
open failure (undef) from an empty-but-successful copy.
The POD now documents the new return-value contract and spells
out that the fix only neutralises 2-arg open() shell-magic;
callers remain responsible for validating attacker-influenced
paths against symlinks, character/block devices (e.g. /dev/zero),
named pipes, and document-root escapes.
Reported and patched by Stig Palmquist (stigtsp).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
t/send-file-magic-open.t exercises four 2-arg open() shell-magic
shapes that map to the CVE-2026-8450 harm table:
- '| cmd' (write-pipe) -- RCE
- 'cmd |' (read-pipe) -- RCE + response-body exfiltration
- ' | cmd' (leading-ws-pipe) -- whitespace-stripping bypass
- '> path' (write-redirect) -- arbitrary file write
plus a bare '<file' shape that under 2-arg open silently strips the
'<' and opens an existing file the caller never named, leaking its
contents to the response body.
For each case, send_file() is invoked on a real
HTTP::Daemon::ClientConn (built via socketpair() and bless) so any
future method dispatch on $self surfaces in the test rather than
silently no-oping against an unblessed scalar filehandle. The pipe
shapes use a marker file as the load-bearing oracle (the marker is
created by the would-be subprocess; absence proves the subprocess
never ran). The marker path is passed to the child via
$ENV{HTTPD_MAGIC_MARKER}, not shell-interpolated, so the test is
robust to spaces or quotes in $TMPDIR. The file is skipped on
Windows where cmd.exe does not honour POSIX single quoting.
A positive control confirms an ordinary file still streams through.
A return-value-contract section confirms send_file() returns '0E0'
(defined, true, numerically zero) for a successful zero-byte copy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The standalone lint workflow set up a bare Perl via shogo82148/actions-setup-perl and installed only Perl::Tidy and App::perlvars — Perl::Critic was missing, so any precious config that exercises perlcritic could not run in CI. Move the lint job into build-and-test.yml as a job that needs build and runs in the perldocker/perl-tester:5.42 container, which already ships perlcritic and perltidy. App::perlvars (niche, not in the image) is still installed via cpm; precious and omegasort still come from oalders/install-ubi-action. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add a perlcritic command to precious.toml that runs only the InputOutput::ProhibitTwoArgOpen policy. --noprofile keeps it from loading any ambient ~/.perlcriticrc, so it runs purely that one policy. Fix the 2-arg piped open the new rule flagged in t/lib/TestServer.pm, switching it to the 3-arg '-|' form (equivalent: the multi-word command string still goes through the shell). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
perl_cmd() now returns the perl binary plus -I flags as a list instead of a single pre-quoted shell string. start() feeds that list to the 3-arg open '-|', so the daemon is exec'd directly with no shell, which drops both the manual space-quoting logic and the shell itself. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
perl_cmd() had exactly one caller and was a one-line sub; fold its body directly into start() and drop the sub. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The pipe family (| cmd, cmd |, " | cmd") collapses to a single risk under 3-arg open: a literal filename that fails to open. Keep one pipe shape -- the leading-whitespace spelling, since it also exercises 2-arg open's leading-whitespace stripping -- next to the distinct write-redirect shape. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.
Summary
HTTP::Daemon::ClientConn::send_file()used 2-argumentopen(FILE, $file), which interprets shell-magic prefixes in the path argument:| cmd/cmd |(command execution),> path/>> path(arbitrary file write),+< path,<& fd, and leading-whitespace variants. Any HTTP::Daemon-based application that passed attacker-influenced bytes tosend_file($string)— e.g. a download endpoint deriving a filename from a query parameter — granted command execution and/or arbitrary file write at the daemon's UID.Tracked as CVE-2026-8450 (affects 6.15 and earlier). Reported and patched by Stig Palmquist (stigtsp).
The fix
send_file()now uses 3-argumentopen(my $fh, '<', $file). The explicit<read mode makes the path argument a literal filename, so every magic shape above is opened as an ordinary file by that exact name (and fails, returningundef).Two collateral hardening changes ride along:
binmode()failure now closes the handle and returnsundef, rather than streaming the file with a wrong PerlIO layer.send_file()returns'0E0'(true zero) on a successful zero-byte transfer, so callers usingsend_file or diecan distinguish an open failure (undef) from an empty-but-successful copy.The POD documents the new return-value contract and spells out that this fix only neutralises 2-arg
open()shell-magic — callers remain responsible for validating attacker-influenced paths against symlinks, character/block devices, named pipes, and document-root escapes.Testing
New
t/send-file-magic-open.tis a regression test covering the pipe, redirect, and bare-<shapes plus positive controls (ordinary file, empty file). It usessocketpairrather than TCP so it runs anywhere.precious.tomlgains aperlcriticpolicy (InputOutput::ProhibitTwoArgOpen) to keep 2-argopen()out of the codebase going forward;t/lib/TestServer.pmwas updated to satisfy it.🤖 Generated with Claude Code