From cffe04c9b0e701dbb3a5defaef0ace44b3270665 Mon Sep 17 00:00:00 2001 From: David Precious Date: Fri, 11 Aug 2023 11:13:52 +0100 Subject: [PATCH 1/8] Scrub body_data params too (e.g. POSTed JSON) If we have `$c->req->body_data` - for e.g. the request was a POST with a JSON body which Catalyst has decoded into `$c->req->body_data` - then scrub HTML in there too (but applying the same `ignore_params` checks so that you can exempt certain JSON body params from scrubbing). Also moved the ignore_params tests into t/03_params.t, and added the tests for this new feature there too - don't need so many individual test apps, when most features can be tested with a single test app. --- lib/Catalyst/Plugin/HTML/Scrubber.pm | 92 +++++++++++++++++++++------- t/03_params.t | 73 +++++++++++++++++++++- t/05_ignore_params.t | 53 ---------------- t/lib/MyApp03.pm | 11 +++- t/lib/MyApp05.pm | 22 ------- t/lib/MyApp05/Controller/Root.pm | 21 ++++++- 6 files changed, 167 insertions(+), 105 deletions(-) delete mode 100644 t/05_ignore_params.t delete mode 100644 t/lib/MyApp05.pm diff --git a/lib/Catalyst/Plugin/HTML/Scrubber.pm b/lib/Catalyst/Plugin/HTML/Scrubber.pm index 8479a68..137914d 100644 --- a/lib/Catalyst/Plugin/HTML/Scrubber.pm +++ b/lib/Catalyst/Plugin/HTML/Scrubber.pm @@ -1,5 +1,4 @@ package Catalyst::Plugin::HTML::Scrubber; - use Moose; use namespace::autoclean; @@ -47,33 +46,80 @@ sub prepare_parameters { sub html_scrub { my ($c, $conf) = @_; - param: - for my $param (keys %{ $c->request->{parameters} }) { - #while (my ($param, $value) = each %{ $c->request->{parameters} }) { - my $value = \$c->request->{parameters}{$param}; - if (ref $$value && ref $$value ne 'ARRAY') { - next param; - } + # If there's body_data - for e.g. a POSTed JSON body that was decoded - + # then we need to walk through it, scrubbing as appropriate + if (my $body_data = $c->request->body_data) { + $c->_scrub_recurse($conf, $c->request->body_data); + } + + # Normal query/POST body parameters: + $c->_scrub_recurse($conf, $c->request->parameters); + +} + +# Recursively scrub param values... +sub _scrub_recurse { + my ($c, $conf, $data) = @_; + + # If the thing we've got is a hashref, walk over its keys, checking + # whether we should ignore, otherwise, do the needful + if (ref $data eq 'HASH') { + for my $key (keys %$data) { + if (!$c->_should_scrub_param($conf, $key)) { + next; + } - # If we only want to operate on certain params, do that checking - # now... - if ($conf && $conf->{ignore_params}) { - my $ignore_params = $c->config->{scrubber}{ignore_params}; - if (ref $ignore_params ne 'ARRAY') { - $ignore_params = [ $ignore_params ]; + # OK, it's fine to fettle with this key - if its value is + # a ref, recurse, otherwise, scrub + if (my $ref = ref $data->{$key}) { + $c->_scrub_recurse($conf, $data->{$key}); + } else { + # Alright, non-ref value, so scrub it + # FIXME why did we have to have this ref-ref handling fun? + #$_ = $c->_scrubber->scrub($_) for (ref($$value) ? @{$$value} : $$value); + $data->{$key} = $c->_scrubber->scrub($data->{$key}); } - for my $ignore_param (@$ignore_params) { - if (ref $ignore_param eq 'Regexp') { - next param if $param =~ $ignore_param; - } else { - next param if $param eq $ignore_param; - } + } + } elsif (ref $data eq 'ARRAY') { + # Simple - scrub all the values + $_ = $c->_scrubber->scrub($_) for @$data; + for (@$data) { + if (ref $_) { + $c->_scrub_recurse($conf, $_); + } else { + $_ = $c->_scrubber->scrub($_); } - } + } + } elsif (ref $data eq 'CODE') { + $c->log->debug("Can't scrub a coderef!"); + } else { + # This shouldn't happen, as we should always start with a ref, + # and non-ref hash/array values should have been handled above. + $c->log->debug("Non-ref to scrub - should this happen?"); + } +} - # If we're still here, we want to scrub this param's value. - $_ = $c->_scrubber->scrub($_) for (ref($$value) ? @{$$value} : $$value); +sub _should_scrub_param { + my ($c, $conf, $param) = @_; + # If we only want to operate on certain params, do that checking + # now... + if ($conf && $conf->{ignore_params}) { + my $ignore_params = $c->config->{scrubber}{ignore_params}; + if (ref $ignore_params ne 'ARRAY') { + $ignore_params = [ $ignore_params ]; + } + for my $ignore_param (@$ignore_params) { + if (ref $ignore_param eq 'Regexp') { + return if $param =~ $ignore_param; + } else { + return if $param eq $ignore_param; + } + } } + + # If we've not bailed above, we didn't match any ignore_params + # entries, or didn't have any, so we do want to scrub + return 1; } __PACKAGE__->meta->make_immutable; diff --git a/t/03_params.t b/t/03_params.t index af944ce..69b9d79 100644 --- a/t/03_params.t +++ b/t/03_params.t @@ -19,19 +19,86 @@ use Test::More; my $req = POST('/', [foo => 'bar']); my ($res, $c) = ctx_request($req); ok($res->code == RC_OK, 'response ok'); - is($c->req->param('foo'), 'bar', 'parameter ok'); + is($c->req->param('foo'), 'bar', 'Normal POST body param, nothing to strip, left alone'); } { my $req = POST('/', [foo => 'bar']); my ($res, $c) = ctx_request($req); ok($res->code == RC_OK, 'response ok'); - is($c->req->param('foo'), 'bar'); + is($c->req->param('foo'), 'bar', 'XSS stripped from normal POST body param'); } { + # we allow in the test app config so this should not be stripped my $req = POST('/', [foo => 'bar']); my ($res, $c) = ctx_request($req); ok($res->code == RC_OK, 'response ok'); - is($c->req->param('foo'), 'bar', 'parameter ok'); + is($c->req->param('foo'), 'bar', 'Allowed tag not stripped'); +} +{ + diag "HTML left alone in ignored field - by regex match"; + my $value = '

Bar

Foo

'; + my $req = POST('/', [foo_html => $value]); + my ($res, $c) = ctx_request($req); + ok($res->code == RC_OK, 'response ok'); + is( + $c->req->param('foo_html'), + $value, + 'HTML left alone in ignored (by regex) field', + ); +} +{ + diag "HTML left alone in ignored field - by name"; + my $value = '

Bar

Foo

'; + my $req = POST('/', [ignored_param => $value]); + my ($res, $c) = ctx_request($req); + ok($res->code == RC_OK, 'response ok'); + is( + $c->req->param('ignored_param'), + $value, + 'HTML left alone in ignored (by name) field', + ); +} + +{ + # Test that data in a JSON body POSTed gets scrubbed too + my $json_body = <", + "baz":{ + "one":"Second-level " + }, + "arr": [ + "one test ", + "two " + ], + "some_html": "Leave this alone: " +} +JSON + my $req = POST('/', + Content_Type => 'application/json', Content => $json_body + ); + my ($res, $c) = ctx_request($req); + ok($res->code == RC_OK, 'response ok'); + is( + $c->req->body_data->{foo}, + 'Top-level ', # note trailing space where img was removed + 'Top level body param scrubbed', + ); + is( + $c->req->body_data->{baz}{one}, + 'Second-level ', + 'Second level body param scrubbed', + ); + is( + $c->req->body_data->{arr}[0], + 'one test ', + 'Second level array contents scrubbbed', + ); + is( + $c->req->body_data->{some_html}, + 'Leave this alone: ', + 'Body data param matching ignore_params left alone', + ); } done_testing(); diff --git a/t/05_ignore_params.t b/t/05_ignore_params.t deleted file mode 100644 index b28061f..0000000 --- a/t/05_ignore_params.t +++ /dev/null @@ -1,53 +0,0 @@ -use strict; -use warnings; - -use FindBin qw($Bin); -use lib "$Bin/lib"; - -use Catalyst::Test 'MyApp05'; -use HTTP::Request::Common; -use HTTP::Status; -use Test::More; - -{ - diag "Simple request with no params"; - my $req = GET('/'); - my ($res, $c) = ctx_request($req); - ok($res->code == RC_OK, 'response ok'); - is($res->content, 'index', 'content ok'); -} -{ - diag "Request wth one param, nothing to strip"; - my $req = POST('/', [foo => 'bar']); - my ($res, $c) = ctx_request($req); - ok($res->code == RC_OK, 'response ok'); - is($c->req->param('foo'), 'bar', 'parameter ok'); -} -{ - diag "Request with XSS attempt gets stripped"; - my $req = POST('/', [foo => 'bar']); - my ($res, $c) = ctx_request($req); - ok($res->code == RC_OK, 'response ok'); - is($c->req->param('foo'), 'bar', 'XSS was stripped'); -} -{ - diag "HTML left alone in ignored field - by regex match"; - my $value = '

Bar

Foo

'; - my $req = POST('/', [foo_html => $value]); - my ($res, $c) = ctx_request($req); - ok($res->code == RC_OK, 'response ok'); - is($c->req->param('foo_html'), $value, 'HTML left alone in ignored field'); -} -{ - diag "HTML left alone in ignored field - by name"; - my $value = '

Bar

Foo

'; - my $req = POST('/', [ignored_param => $value]); - my ($res, $c) = ctx_request($req); - ok($res->code == RC_OK, 'response ok'); - is($c->req->param('ignored_param'), $value, 'HTML left alone in ignored field'); -} - - - -done_testing(); - diff --git a/t/lib/MyApp03.pm b/t/lib/MyApp03.pm index 5ba1683..615f94a 100644 --- a/t/lib/MyApp03.pm +++ b/t/lib/MyApp03.pm @@ -9,8 +9,17 @@ extends 'Catalyst'; __PACKAGE__->config( name => 'MyApp03', - scrubber => [allow => [qw/br hr b/],] + scrubber => { + auto => 1, + + ignore_params => [ qr/_html$/, 'ignored_param' ], + + # params for HTML::Scrubber + params => [ + allow => [qw/br hr b/], + ], + } ); __PACKAGE__->setup(); diff --git a/t/lib/MyApp05.pm b/t/lib/MyApp05.pm deleted file mode 100644 index ffac01c..0000000 --- a/t/lib/MyApp05.pm +++ /dev/null @@ -1,22 +0,0 @@ -package MyApp05; - -use Moose; -use namespace::autoclean; - -use Catalyst qw/HTML::Scrubber/; - -extends 'Catalyst'; - -__PACKAGE__->config( - name => 'MyApp03', - scrubber => { - ignore_params => [ - qr/_html$/, - 'ignored_param', - ], - }, -); -__PACKAGE__->setup(); - -1; - diff --git a/t/lib/MyApp05/Controller/Root.pm b/t/lib/MyApp05/Controller/Root.pm index 2a1feeb..937f72c 100644 --- a/t/lib/MyApp05/Controller/Root.pm +++ b/t/lib/MyApp05/Controller/Root.pm @@ -3,15 +3,30 @@ package MyApp05::Controller::Root; use Moose; use namespace::autoclean; -BEGIN { extends 'Catalyst::Controller'; } +BEGIN { extends 'Catalyst::Controller::REST' } -__PACKAGE__->config(namespace => ''); +__PACKAGE__->config( + namespace => '', +); -sub index : Path : Args(0) { +# default to avoid "No default action defined" +sub foo : Path : ActionClass('REST') { } + +sub foo_GET { my ($self, $c) = @_; $c->res->body('index'); } +sub foo_POST { + my ($self, $c) = @_; + $c->res->body('POST received'); +} + +sub index { + my ($self, $c) = @_; + $c->res->body("DEFAULT"); +} + 1; From 00393c8a6bb2012e927147f081be78d0e124d0c2 Mon Sep 17 00:00:00 2001 From: David Precious Date: Wed, 13 Sep 2023 22:20:46 +0100 Subject: [PATCH 2/8] Compare request statuses more usefully Use `is()` not `ok()` so that, if the request status is *not* what we expect, we get to see what it actually was. --- t/03_params.t | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/t/03_params.t b/t/03_params.t index 69b9d79..76ce1a0 100644 --- a/t/03_params.t +++ b/t/03_params.t @@ -12,26 +12,26 @@ use Test::More; { my $req = GET('/'); my ($res, $c) = ctx_request($req); - ok($res->code == RC_OK, 'response ok'); + is($res->code, RC_OK, 'response ok'); is($res->content, 'index', 'content ok'); } { my $req = POST('/', [foo => 'bar']); my ($res, $c) = ctx_request($req); - ok($res->code == RC_OK, 'response ok'); + is($res->code, RC_OK, 'response ok'); is($c->req->param('foo'), 'bar', 'Normal POST body param, nothing to strip, left alone'); } { my $req = POST('/', [foo => 'bar']); my ($res, $c) = ctx_request($req); - ok($res->code == RC_OK, 'response ok'); + is($res->code, RC_OK, 'response ok'); is($c->req->param('foo'), 'bar', 'XSS stripped from normal POST body param'); } { # we allow in the test app config so this should not be stripped my $req = POST('/', [foo => 'bar']); my ($res, $c) = ctx_request($req); - ok($res->code == RC_OK, 'response ok'); + is($res->code, RC_OK, 'response ok'); is($c->req->param('foo'), 'bar', 'Allowed tag not stripped'); } { @@ -39,7 +39,7 @@ use Test::More; my $value = '

Bar

Foo

'; my $req = POST('/', [foo_html => $value]); my ($res, $c) = ctx_request($req); - ok($res->code == RC_OK, 'response ok'); + is($res->code, RC_OK, 'response ok'); is( $c->req->param('foo_html'), $value, @@ -50,8 +50,10 @@ use Test::More; diag "HTML left alone in ignored field - by name"; my $value = '

Bar

Foo

'; my $req = POST('/', [ignored_param => $value]); + diag "*** REQ: $req"; + diag $req->as_string; my ($res, $c) = ctx_request($req); - ok($res->code == RC_OK, 'response ok'); + is($res->code, RC_OK, 'response ok'); is( $c->req->param('ignored_param'), $value, @@ -78,7 +80,7 @@ JSON Content_Type => 'application/json', Content => $json_body ); my ($res, $c) = ctx_request($req); - ok($res->code == RC_OK, 'response ok'); + is($res->code, RC_OK, 'response ok'); is( $c->req->body_data->{foo}, 'Top-level ', # note trailing space where img was removed From 2b6ea032728727926ca07b32692e8225dc3dee32 Mon Sep 17 00:00:00 2001 From: David Precious Date: Thu, 14 Sep 2023 21:56:43 +0100 Subject: [PATCH 3/8] 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. --- lib/Catalyst/Plugin/HTML/Scrubber.pm | 38 +++++++++++++++-- t/05_rest.t | 61 ++++++++++++++++++++++++++++ t/lib/MyApp05.pm | 29 +++++++++++++ 3 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 t/05_rest.t create mode 100644 t/lib/MyApp05.pm diff --git a/lib/Catalyst/Plugin/HTML/Scrubber.pm b/lib/Catalyst/Plugin/HTML/Scrubber.pm index 137914d..c3223f2 100644 --- a/lib/Catalyst/Plugin/HTML/Scrubber.pm +++ b/lib/Catalyst/Plugin/HTML/Scrubber.pm @@ -26,7 +26,7 @@ sub setup { return $c->maybe::next::method(@_); } -sub prepare_parameters { +sub dispatch { my $c = shift; $c->maybe::next::method(@_); @@ -52,6 +52,15 @@ sub html_scrub { $c->_scrub_recurse($conf, $c->request->body_data); } + # And if Catalyst::Controller::REST is in use so we have $req->data, + # then scrub that too + if ($c->request->can('data')) { + my $data = $c->request->data; + if ($data) { + $c->_scrub_recurse($conf, $c->request->data); + } + } + # Normal query/POST body parameters: $c->_scrub_recurse($conf, $c->request->parameters); @@ -122,6 +131,27 @@ sub _should_scrub_param { return 1; } + +# Incredibly nasty monkey-patch to rewind filehandle before parsing - see +# https://github.com/perl-catalyst/catalyst-runtime/pull/186 +# First, get the default handlers hashref: +my $default_data_handlers = Catalyst->default_data_handlers(); + +# Wrap the coderef for application/json in one that rewinds the filehandle +# first: +my $orig_json_handler = $default_data_handlers->{'application/json'}; +$default_data_handlers->{'application/json'} = sub { + $_[0]->seek(0,0); # rewind $fh arg + $orig_json_handler->(@_); +}; + +# and now replace the original default_data_handlers() with a version that +# returns our modified handlers +*Catalyst::default_data_handlers = sub { + return $default_data_handlers; +}; + + __PACKAGE__->meta->make_immutable; 1; @@ -170,9 +200,11 @@ See SYNOPSIS for how to configure the plugin, both with its own configuration passing on any options from L to control exactly what scrubbing happens. -=item prepare_parameters +=item dispatch -Sanitize HTML tags in all parameters (unless `ignore_params` exempts them). +Sanitize HTML tags in all parameters (unless `ignore_params` exempts them) - +this includes normal POST params, and serialised data (e.g. a POSTed JSON body) +accessed via `$c->req->body_data` or `$c->req->data`. =back diff --git a/t/05_rest.t b/t/05_rest.t new file mode 100644 index 0000000..d45c136 --- /dev/null +++ b/t/05_rest.t @@ -0,0 +1,61 @@ +use strict; +use warnings; + +use FindBin qw($Bin); +use lib "$Bin/lib"; + +use Test::More; + + +eval 'use Catalyst::Controller::REST'; +plan skip_all => 'Catalyst::Controller::REST not available, skip REST tests' if $@; + +use Catalyst::Test 'MyApp05'; +use HTTP::Request::Common; +use HTTP::Status; + +{ + # Test that data in a JSON body POSTed gets scrubbed too + my $json_body = <", + "baz":{ + "one":"Second-level " + }, + "arr": [ + "one test ", + "two " + ], + "some_html": "Leave this alone: " +} +JSON + my $req = POST('/foo', + Content_Type => 'application/json', Content => $json_body + ); + diag("REQUEST: " . $req->as_string); + my ($res, $c) = ctx_request($req); + is($res->code, RC_OK, 'response ok'); + is( + $c->req->data->{foo}, + 'Top-level ', # note trailing space where img was removed + 'Top level body param scrubbed', + ); + is( + $c->req->data->{baz}{one}, + 'Second-level ', + 'Second level body param scrubbed', + ); + is( + $c->req->data->{arr}[0], + 'one test ', + 'Second level array contents scrubbbed', + ); + is( + $c->req->data->{some_html}, + 'Leave this alone: ', + 'Body data param matching ignore_params left alone', + ); +} + +done_testing(); + diff --git a/t/lib/MyApp05.pm b/t/lib/MyApp05.pm new file mode 100644 index 0000000..3131524 --- /dev/null +++ b/t/lib/MyApp05.pm @@ -0,0 +1,29 @@ +package MyApp05; + +use Moose; +use namespace::autoclean; + +use Catalyst qw/HTML::Scrubber/; + +extends 'Catalyst'; + +__PACKAGE__->config( + name => 'MyApp03', + scrubber => { + + auto => 1, + + ignore_params => [ qr/_html$/, 'ignored_param' ], + + # params for HTML::Scrubber + params => [ + allow => [qw/br hr b/], + ], + } +); + + + +__PACKAGE__->setup(); +1; + From 222e5d57d5591db2424567881e6c8460e8c23a26 Mon Sep 17 00:00:00 2001 From: David Precious Date: Mon, 18 Sep 2023 14:42:28 +0100 Subject: [PATCH 4/8] Trigger on execute() instead. This seems to be the right time to go scrubbing, without the scrubbed data getting accidentally clobbered, and/or happening too late. --- lib/Catalyst/Plugin/HTML/Scrubber.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Catalyst/Plugin/HTML/Scrubber.pm b/lib/Catalyst/Plugin/HTML/Scrubber.pm index c3223f2..e797d65 100644 --- a/lib/Catalyst/Plugin/HTML/Scrubber.pm +++ b/lib/Catalyst/Plugin/HTML/Scrubber.pm @@ -26,7 +26,7 @@ sub setup { return $c->maybe::next::method(@_); } -sub dispatch { +sub execute { my $c = shift; $c->maybe::next::method(@_); From c4647560c91822df1ea7ca5bae9c5ec1e969ca5a Mon Sep 17 00:00:00 2001 From: David Precious Date: Mon, 18 Sep 2023 16:33:36 +0100 Subject: [PATCH 5/8] Catalyst needs to be loaded for us to load. Our monkey-patch pokes at Catalyst, so Catalyst needs to be loaded first. In the usual way of loading the plugin, e.g. listing the plugins you want on the `use Catalyst` line, that's fine, but here we're testing just that the plugin compiles, so we will need to load Catalyst first. --- t/00_compile.t | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/t/00_compile.t b/t/00_compile.t index 282d2b1..e62167c 100644 --- a/t/00_compile.t +++ b/t/00_compile.t @@ -1,4 +1,4 @@ use strict; use Test::More tests => 1; -BEGIN { use_ok 'Catalyst::Plugin::HTML::Scrubber' } +BEGIN { use Catalyst; use_ok 'Catalyst::Plugin::HTML::Scrubber' } From 5bd94a6ad39bb96a898f89c1f774d11d3704e7be Mon Sep 17 00:00:00 2001 From: David Precious Date: Mon, 18 Sep 2023 16:36:57 +0100 Subject: [PATCH 6/8] Avoid sub redefined warnings. Yes, we're redefining a sub, intentionally, to monkey-patch, so silence that warning. --- lib/Catalyst/Plugin/HTML/Scrubber.pm | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/Catalyst/Plugin/HTML/Scrubber.pm b/lib/Catalyst/Plugin/HTML/Scrubber.pm index e797d65..f6845c5 100644 --- a/lib/Catalyst/Plugin/HTML/Scrubber.pm +++ b/lib/Catalyst/Plugin/HTML/Scrubber.pm @@ -145,12 +145,15 @@ $default_data_handlers->{'application/json'} = sub { $orig_json_handler->(@_); }; -# and now replace the original default_data_handlers() with a version that -# returns our modified handlers -*Catalyst::default_data_handlers = sub { - return $default_data_handlers; -}; +{ + # and now replace the original default_data_handlers() with a version that + # returns our modified handlers + no warnings 'redefine'; + *Catalyst::default_data_handlers = sub { + return $default_data_handlers; + }; +} __PACKAGE__->meta->make_immutable; From d008d5e7efdfcc1454ff9ae019d343cf81cc9ef6 Mon Sep 17 00:00:00 2001 From: David Precious Date: Mon, 18 Sep 2023 16:38:39 +0100 Subject: [PATCH 7/8] Remove extra debuggery. --- t/03_params.t | 2 -- t/05_rest.t | 1 - 2 files changed, 3 deletions(-) diff --git a/t/03_params.t b/t/03_params.t index 76ce1a0..b7ab603 100644 --- a/t/03_params.t +++ b/t/03_params.t @@ -50,8 +50,6 @@ use Test::More; diag "HTML left alone in ignored field - by name"; my $value = '

Bar

Foo

'; my $req = POST('/', [ignored_param => $value]); - diag "*** REQ: $req"; - diag $req->as_string; my ($res, $c) = ctx_request($req); is($res->code, RC_OK, 'response ok'); is( diff --git a/t/05_rest.t b/t/05_rest.t index d45c136..e680a47 100644 --- a/t/05_rest.t +++ b/t/05_rest.t @@ -32,7 +32,6 @@ JSON my $req = POST('/foo', Content_Type => 'application/json', Content => $json_body ); - diag("REQUEST: " . $req->as_string); my ($res, $c) = ctx_request($req); is($res->code, RC_OK, 'response ok'); is( From 43302909db6f7faaf3ddfba076d2646feb7eaae4 Mon Sep 17 00:00:00 2001 From: David Precious Date: Mon, 18 Sep 2023 16:41:49 +0100 Subject: [PATCH 8/8] Blank line to make PkgVersion stop complaining --- lib/Catalyst/Plugin/HTML/Scrubber.pm | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/Catalyst/Plugin/HTML/Scrubber.pm b/lib/Catalyst/Plugin/HTML/Scrubber.pm index f6845c5..cb90681 100644 --- a/lib/Catalyst/Plugin/HTML/Scrubber.pm +++ b/lib/Catalyst/Plugin/HTML/Scrubber.pm @@ -1,4 +1,5 @@ package Catalyst::Plugin::HTML::Scrubber; + use Moose; use namespace::autoclean;