Skip to content

Commit 1e54cf9

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 1e54cf9

File tree

4 files changed

+155
-1
lines changed

4 files changed

+155
-1
lines changed

Diff for: lib/Catalyst/Plugin/HTML/Scrubber.pm

+11-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ sub setup {
2626
return $c->maybe::next::method(@_);
2727
}
2828

29-
sub prepare_parameters {
29+
#sub prepare_parameters {
30+
sub dispatch {
3031
my $c = shift;
3132

3233
$c->maybe::next::method(@_);
@@ -52,6 +53,15 @@ sub html_scrub {
5253
$c->_scrub_recurse($conf, $c->request->body_data);
5354
}
5455

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

Diff for: t/05_rest.t

+61
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+

Diff for: t/lib/MyApp05.pm

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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+
# Incredibly nasty monkey-patch to rewind filehandle before parsing - see
27+
# https://github.com/perl-catalyst/catalyst-runtime/pull/186
28+
# First, get the default handlers hashref:
29+
my $default_data_handlers = __PACKAGE__->default_data_handlers();
30+
31+
# Wrap the coderef for application/json in one that rewinds the filehandle
32+
# first:
33+
my $orig_json_handler = $default_data_handlers->{'application/json'};
34+
$default_data_handlers->{'application/json'} = sub {
35+
$_[0]->seek(0,0); # rewind $fh arg
36+
$orig_json_handler->(@_);
37+
};
38+
39+
# and now replace the original default_data_handlers() with a version that
40+
# returns our modified handlers
41+
*Catalyst::default_data_handlers = sub {
42+
return $default_data_handlers;
43+
};
44+
45+
46+
47+
__PACKAGE__->setup();
48+
1;
49+

Diff for: t/lib/MyApp05/Controller/Root.pm

+34
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)