Skip to content

Commit 95573dc

Browse files
jjn1056claude
andcommitted
fix: escape scope values in Debug middleware panel (XSS)
Method, path, query_string, and scheme were interpolated into HTML without escaping. Apply the existing _html_escape() helper to all scope values, matching how headers were already handled. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3054c02 commit 95573dc

2 files changed

Lines changed: 74 additions & 4 deletions

File tree

lib/PAGI/Middleware/Debug.pm

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -215,10 +215,10 @@ sub _build_panel {
215215

216216
# Request section
217217
if ($self->{show_scope}) {
218-
my $method = $scope->{method} // '';
219-
my $path = $scope->{path} // '';
220-
my $query = $scope->{query_string} // '';
221-
my $scheme = $scope->{scheme} // '';
218+
my $method = _html_escape($scope->{method});
219+
my $path = _html_escape($scope->{path});
220+
my $query = _html_escape($scope->{query_string});
221+
my $scheme = _html_escape($scope->{scheme});
222222
$html .= qq{
223223
<div class="section">
224224
<div class="section-title">Request</div>

t/middleware/debug-xss.t

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
use strict;
2+
use warnings;
3+
use Test2::V0;
4+
use PAGI::Middleware::Debug;
5+
use Future::AsyncAwait;
6+
7+
# Test that scope values are HTML-escaped in the debug panel
8+
9+
my $debug = PAGI::Middleware::Debug->new(
10+
enabled => 1,
11+
show_scope => 1,
12+
show_headers => 0,
13+
show_timing => 0,
14+
);
15+
16+
# Build a scope with XSS payloads in every scope value
17+
my $xss = '<script>alert("xss")</script>';
18+
my $scope = {
19+
type => 'http',
20+
method => $xss,
21+
path => $xss,
22+
query_string => $xss,
23+
scheme => $xss,
24+
headers => [],
25+
};
26+
27+
# Capture the response body from the debug panel
28+
my $captured_body;
29+
30+
my $inner_app = async sub {
31+
my ($scope, $receive, $send) = @_;
32+
await $send->({
33+
type => 'http.response.start',
34+
status => 200,
35+
headers => [['content-type', 'text/html']],
36+
});
37+
await $send->({
38+
type => 'http.response.body',
39+
body => '<html><body>Hello</body></html>',
40+
more => 0,
41+
});
42+
};
43+
44+
my $wrapped = $debug->wrap($inner_app);
45+
46+
my $send = async sub {
47+
my ($event) = @_;
48+
if ($event->{type} eq 'http.response.body') {
49+
$captured_body = $event->{body};
50+
}
51+
};
52+
53+
my $receive = async sub { return { type => 'http.disconnect' } };
54+
55+
# Run the middleware
56+
$wrapped->($scope, $receive, $send)->get;
57+
58+
# The raw XSS string must NOT appear in the output
59+
unlike($captured_body, qr/<script>alert/, 'no raw script tags in debug panel output');
60+
61+
# The escaped version MUST appear
62+
like($captured_body, qr/&lt;script&gt;alert/, 'XSS payload is HTML-escaped in debug panel');
63+
64+
# Verify each field individually
65+
like($captured_body, qr{<th>Method</th><td>&lt;script&gt;}, 'method field is escaped');
66+
like($captured_body, qr{<th>Path</th><td>&lt;script&gt;}, 'path field is escaped');
67+
like($captured_body, qr{<th>Query</th><td>&lt;script&gt;}, 'query field is escaped');
68+
like($captured_body, qr{<th>Scheme</th><td>&lt;script&gt;}, 'scheme field is escaped');
69+
70+
done_testing;

0 commit comments

Comments
 (0)