Skip to content

Commit 069310c

Browse files
committed
Support scrubbing $c->req->data from C::Action::REST
If Catalyst::Action::REST / Catalyst::Controller::REST is in use, the request object will have a `data()` method for deserialised data as added by the Catalyst::TraitFor::Request::REST role which ought to be scrubbed too. To support this, (a) the scrubbing needs to happen later in the request flow - now `hooking dispatch()` instead of `prepare_parameters()` (b) to avoid the data not being read if the request body had already been read by `$c->req->body_data`, the fix in this PR is needed: perl-catalyst/catalyst-runtime/pull/186 Until such time, dirtily monkey-patch the `seek()` in.
1 parent 7bac66b commit 069310c

File tree

4 files changed

+159
-3
lines changed

4 files changed

+159
-3
lines changed

lib/Catalyst/Plugin/HTML/Scrubber.pm

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ sub setup {
2626
return $c->maybe::next::method(@_);
2727
}
2828

29-
sub prepare_parameters {
29+
sub dispatch {
3030
my $c = shift;
3131

3232
$c->maybe::next::method(@_);
@@ -52,6 +52,15 @@ sub html_scrub {
5252
$c->_scrub_recurse($conf, $c->request->body_data);
5353
}
5454

55+
# And if Catalyst::Controller::REST is in use so we have $req->data,
56+
# then scrub that too
57+
if ($c->request->can('data')) {
58+
my $data = $c->request->data;
59+
if ($data) {
60+
$c->_scrub_recurse($conf, $c->request->data);
61+
}
62+
}
63+
5564
# Normal query/POST body parameters:
5665
$c->_scrub_recurse($conf, $c->request->parameters);
5766

@@ -122,6 +131,27 @@ sub _should_scrub_param {
122131
return 1;
123132
}
124133

134+
135+
# Incredibly nasty monkey-patch to rewind filehandle before parsing - see
136+
# https://github.com/perl-catalyst/catalyst-runtime/pull/186
137+
# First, get the default handlers hashref:
138+
my $default_data_handlers = Catalyst->default_data_handlers();
139+
140+
# Wrap the coderef for application/json in one that rewinds the filehandle
141+
# first:
142+
my $orig_json_handler = $default_data_handlers->{'application/json'};
143+
$default_data_handlers->{'application/json'} = sub {
144+
$_[0]->seek(0,0); # rewind $fh arg
145+
$orig_json_handler->(@_);
146+
};
147+
148+
# and now replace the original default_data_handlers() with a version that
149+
# returns our modified handlers
150+
*Catalyst::default_data_handlers = sub {
151+
return $default_data_handlers;
152+
};
153+
154+
125155
__PACKAGE__->meta->make_immutable;
126156

127157
1;
@@ -170,9 +200,11 @@ See SYNOPSIS for how to configure the plugin, both with its own configuration
170200
passing on any options from L<HTML::Scrubber> to control exactly what
171201
scrubbing happens.
172202
173-
=item prepare_parameters
203+
=item dispatch
174204
175-
Sanitize HTML tags in all parameters (unless `ignore_params` exempts them).
205+
Sanitize HTML tags in all parameters (unless `ignore_params` exempts them) -
206+
this includes normal POST params, and serialised data (e.g. a POSTed JSON body)
207+
accessed via `$c->req->body_data` or `$c->req->data`.
176208
177209
=back
178210

t/05_rest.t

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
use strict;
2+
use warnings;
3+
4+
use FindBin qw($Bin);
5+
use lib "$Bin/lib";
6+
7+
use Test::More;
8+
9+
10+
eval 'use Catalyst::Controller::REST';
11+
plan skip_all => 'Catalyst::Controller::REST not available, skip REST tests' if $@;
12+
13+
use Catalyst::Test 'MyApp05';
14+
use HTTP::Request::Common;
15+
use HTTP::Status;
16+
17+
{
18+
# Test that data in a JSON body POSTed gets scrubbed too
19+
my $json_body = <<JSON;
20+
{
21+
"foo": "Top-level <img src=foo.jpg title=fun>",
22+
"baz":{
23+
"one":"Second-level <img src=test.jpg>"
24+
},
25+
"arr": [
26+
"one test <img src=arrtest1.jpg>",
27+
"two <script>window.alert('XSS!');</script>"
28+
],
29+
"some_html": "Leave <b>this</b> alone: <img src=allowed.gif>"
30+
}
31+
JSON
32+
my $req = POST('/foo',
33+
Content_Type => 'application/json', Content => $json_body
34+
);
35+
diag("REQUEST: " . $req->as_string);
36+
my ($res, $c) = ctx_request($req);
37+
is($res->code, RC_OK, 'response ok');
38+
is(
39+
$c->req->data->{foo},
40+
'Top-level ', # note trailing space where img was removed
41+
'Top level body param scrubbed',
42+
);
43+
is(
44+
$c->req->data->{baz}{one},
45+
'Second-level ',
46+
'Second level body param scrubbed',
47+
);
48+
is(
49+
$c->req->data->{arr}[0],
50+
'one test ',
51+
'Second level array contents scrubbbed',
52+
);
53+
is(
54+
$c->req->data->{some_html},
55+
'Leave <b>this</b> alone: <img src=allowed.gif>',
56+
'Body data param matching ignore_params left alone',
57+
);
58+
}
59+
60+
done_testing();
61+

t/lib/MyApp05.pm

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package MyApp05;
2+
3+
use Moose;
4+
use namespace::autoclean;
5+
6+
use Catalyst qw/HTML::Scrubber/;
7+
8+
extends 'Catalyst';
9+
10+
__PACKAGE__->config(
11+
name => 'MyApp03',
12+
scrubber => {
13+
14+
auto => 1,
15+
16+
ignore_params => [ qr/_html$/, 'ignored_param' ],
17+
18+
# params for HTML::Scrubber
19+
params => [
20+
allow => [qw/br hr b/],
21+
],
22+
}
23+
);
24+
25+
26+
27+
__PACKAGE__->setup();
28+
1;
29+

t/lib/MyApp05/Controller/Root.pm

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package MyApp05::Controller::Root;
2+
3+
use Moose;
4+
use namespace::autoclean;
5+
6+
BEGIN { extends 'Catalyst::Controller::REST' }
7+
8+
__PACKAGE__->config(
9+
namespace => '',
10+
);
11+
12+
# default to avoid "No default action defined"
13+
sub foo : Path : ActionClass('REST') { }
14+
15+
sub foo_GET {
16+
my ($self, $c) = @_;
17+
18+
$c->res->body('index');
19+
}
20+
21+
sub foo_POST {
22+
my ($self, $c) = @_;
23+
$c->log->debug("index_POST running for a " . $c->req->method . " request");
24+
$c->res->body('POST received');
25+
}
26+
27+
sub index {
28+
my ($self, $c) = @_;
29+
$c->log->debug("Default index route hit by " . $c->req->method . " request");
30+
$c->res->body("DEFAULT");
31+
}
32+
33+
1;
34+

0 commit comments

Comments
 (0)