diff --git a/lib/Catalyst.pm b/lib/Catalyst.pm index 9a886e6..da26947 100644 --- a/lib/Catalyst.pm +++ b/lib/Catalyst.pm @@ -1913,8 +1913,9 @@ sub prepare_body { if ( $c->debug && keys %{ $c->req->body_parameters } ) { my $t = Text::SimpleTable->new( [ 35, 'Parameter' ], [ 36, 'Value' ] ); - for my $key ( sort keys %{ $c->req->body_parameters } ) { - my $param = $c->req->body_parameters->{$key}; + my $filtered_params = $c->apply_parameter_debug_filters('body', $c->req->body_parameters); + for my $key ( sort keys %$filtered_params ) { + my $param = $filtered_params->{$key}; my $value = defined($param) ? $param : ''; $t->row( $key, ref $value eq 'ARRAY' ? ( join ', ', @$value ) : $value ); @@ -2007,8 +2008,9 @@ sub prepare_query_parameters { if ( $c->debug && keys %{ $c->request->query_parameters } ) { my $t = Text::SimpleTable->new( [ 35, 'Parameter' ], [ 36, 'Value' ] ); - for my $key ( sort keys %{ $c->req->query_parameters } ) { - my $param = $c->req->query_parameters->{$key}; + my $filtered_params = $c->apply_parameter_debug_filters('query', $c->req->query_parameters); + for my $key ( sort keys %{ $filtered_params } ) { + my $param = $filtered_params->{$key}; my $value = defined($param) ? $param : ''; $t->row( $key, ref $value eq 'ARRAY' ? ( join ', ', @$value ) : $value ); @@ -2017,6 +2019,173 @@ sub prepare_query_parameters { } } +=head2 $c->apply_parameter_debug_filters($params) + +Applies configured filters to parameter list for debug output. + +If you have sensitive data that you do not want written to the Catalyst +debug logs, you can set options in your config to filter those values out. +There are a few different ways you can set these up depending on what +exactly you need to filter. + +=head3 Filtering parameters by name + +The most basic means of filtering is to add an entry into your config +as shown below. You can have a simple scalar to just filter a +single parameter or an ARRAY ref to filter out multiple params. + + # filters a single param + __PACKAGE__->config->{debug}->{filter_params} = 'param_name'; + + # filters multiple params + __PACKAGE__->config->{debug}->{filter_params} = [qw(param1 param2)]; + +When the debug logs are generated for a given request, any parameters +(query or body) that exactly match the specified value(s) will have +their values replaced with '[FILTERED]'. For instance: + + [debug] Query Parameters are: + .-------------------------------------+--------------------------------------. + | Parameter | Value | + +-------------------------------------+--------------------------------------+ + | param_name | [FILTERED] | + .-------------------------------------+--------------------------------------. + +=head3 Filtering parameters by regular expression + +If you have a set of parameters you need to filter, you can specify a +regular expression that will be used to match against parameter names. + + # filters parameters starting with "private." + __PACKAGE__->config->{debug}->{filter_params} = qr/^private\./ + + # filters parameters named "param1" or starting with "private." or "secret." + __PACKAGE__->config->{debug}->{filter_params} = [ 'param1', qr/^private\./, qr/^secret\./ ] + +Notice on the second example, the ARRAY ref contains a string as well +as two regular expressions. This should DWIM and filter parameters that +match any of the filters specified. + +=head3 Filtering parameters by callback + +If you want even more flexible filtering, you can specify an anonymous +subroutine. The subroutine is given the parameter name and value and +is expected to return the new value that will be show in the debug log. +An C return value indicates that no change should be made to +the value. + + # transform any "password" param to "********" + __PACKAGE__->config->{debug}->{filter_params} = + sub { my ( $k, $v ) = @_; return unless $k eq 'password'; return '*' x 8; }; + + # combine with other filtering methods + __PACKAGE__->config->{debug}->{filter_params} = [ + 'simple_param_name', + qr/^private\./, + sub { my ( $k, $v ) = @_; return unless $k eq 'password'; return '*' x 8; }, + ]; + +An example of the debug log for a request with +C would be: + + [debug] Body Parameters are: + .-------------------------------------+--------------------------------------. + | Parameter | Value | + +-------------------------------------+--------------------------------------+ + | some_other_param | some_other_value | + | password | ******** | + .-------------------------------------+--------------------------------------. + +=head3 Filtering by parameter location + +If you have a different set of filters based on how they were passed +(query vs. body vs. all), you can specify a HASH ref with different sets of +filters: + + # filters all body parameters + __PACKAGE__->config->{debug}->{filter_params}->{body} = qr//; + + # filters query parameters starting with "private." + __PACKAGE__->config->{debug}->{filter_params}->{query} = qr/^private\./ + + # filters all parameters (query or body) through the specified callback + __PACKAGE__->config->{debug}->{filter_params}->{all} = sub { return unless $_[0] eq 'fizzbuzz'; return 'FIZZBUZZ FILTER' }; + +Of course, you can use any of the above filtering methods with these +"location-specific" filters: + + # body parameter filters + __PACKAGE__->config->{debug}->{filter_params}->{body} = [ + 'some_param', + qr/^private\./, + sub { return 'XXX' if shift eq 'other_param' } + ]; + + # query parameter filters + __PACKAGE__->config->{debug}->{filter_params}->{query} = 'some_query_param'; + + # query parameter filters + __PACKAGE__->config->{debug}->{filter_params}->{all} = [qw(foo bar)]; + +=cut + +sub apply_parameter_debug_filters { + my $c = shift; + my $type = shift; + my $params = shift; + + # take a copy since we don't want to modify the original + my $filtered = {%$params}; + + my $filter_param_config = $c->config->{Debug}->{filter_params}; + my @filters; + if(ref($filter_param_config) eq 'HASH'){ + # filters broken out by parameter type (i.e. body, query, all) + my $type_filters = $filter_param_config->{$type} || []; + $type_filters = [$type_filters] if ref $type_filters ne 'ARRAY'; + my $all_filters = $filter_param_config->{'all'} || []; + $all_filters = [$all_filters] if ref $all_filters ne 'ARRAY'; + + @filters = ( @$type_filters, @$all_filters ); + } elsif(ref($filter_param_config) eq 'ARRAY'){ + # standard filters, applied to any parameter type + @filters = @{$filter_param_config}; + } elsif($filter_param_config) { + # a single filter + @filters = ($filter_param_config); + } + + my $filter_str = '[FILTERED]'; + + # apply filters to each param + foreach my $f( @filters){ + + if(!ref($f)){ + # simple key lookup + $filtered->{$f} = $filter_str if(exists($filtered->{$f})); + } elsif(ref($f) eq 'Regexp'){ + # match on regex + foreach my $k(grep {$_ =~ $f} keys %$filtered){ + $filtered->{$k} = $filter_str; + } + } elsif(ref($f) eq 'CODE'){ + # allow callback to modify each parameter + foreach my $k(keys %$filtered){ + # take a copy of the key to avoid the callback inadvertantly + # modifying things + my $copy_key = $k; + + my $returned = $f->($copy_key => $filtered->{$k}); + + # if no value is returned, we assume the filter chose not to modify anything + # otherwise, the returned value is the logged value + $filtered->{$k} = $returned if defined $returned; + } + } + } + return $filtered; +} + =head2 $c->prepare_read Prepares the input for reading. diff --git a/t/unit_core_apply_param_filters.t b/t/unit_core_apply_param_filters.t new file mode 100644 index 0000000..b2f9ce2 --- /dev/null +++ b/t/unit_core_apply_param_filters.t @@ -0,0 +1,47 @@ +use strict; +use warnings; +use Test::More tests=>12; + +use Catalyst; +my $context = Catalyst->new( {} ); +$context->config->{debug}->{filter_params} = 'simple_str'; + +isa_ok( $context, 'Catalyst' ); +my $params = $context->apply_parameter_debug_filters( 'query', {} ); +is_deeply( $params, {}, 'empty param list' ); +my $filter_str = '[FILTERED]'; + +$params = $context->apply_parameter_debug_filters( 'body', { simple_str => 1, other_str => 2 } ); +is( $params->{simple_str}, $filter_str, 'filtered simple_str' ); +is( $params->{other_str}, '2', "didn't filter other_str" ); + +$context->config->{debug}->{filter_params} = [qw(a b)]; +$params = $context->apply_parameter_debug_filters( 'query', { a => 1, b => 2, c => 3 }, ); + +is_deeply( $params, { a => $filter_str, b => $filter_str, c => 3 }, 'list of simple param names' ); + +$context->config->{debug}->{filter_params} = qr/^foo/; +$params = $context->apply_parameter_debug_filters( 'query', { foo => 1, foobar => 2, c => 3 }, ); +is_deeply( $params, { foo => $filter_str, foobar => $filter_str, c => 3 }, 'single regex' ); + +$context->config->{debug}->{filter_params} = [qr/^foo/, qr/bar/, 'simple']; +$params = $context->apply_parameter_debug_filters( 'query', { foo => 1, foobar => 2, bar => 3, c => 3, simple => 4 }, ); +is_deeply( $params, { foo => $filter_str, foobar => $filter_str, bar => $filter_str, c => 3, simple => $filter_str }, 'array of regexes and a simple filter' ); + +$context->config->{debug}->{filter_params} = sub { return unless shift eq 'password'; return '*' x 8 }; +$params = $context->apply_parameter_debug_filters( 'query', { password => 'secret', other => 'public' }, ); +is_deeply( $params, { other => 'public', password => '********' }, 'single CODE ref' ); + +$context->config->{debug}->{filter_params} = {}; +$context->config->{debug}->{filter_params}->{body} = qr//; +$params = $context->apply_parameter_debug_filters( 'query', { a=>1, b=>2 } ); +is_deeply( $params, { a=>1, b=>2 }, 'body filters do not modify query params' ); +$params = $context->apply_parameter_debug_filters( 'body', { a=>1, b=>2 } ); +is_deeply( $params, { a => $filter_str, b => $filter_str }, 'all body params filtered' ); + +delete($context->config->{debug}->{filter_params}->{body}); +$context->config->{debug}->{filter_params}->{all} = [qw(foo bar)]; +$params = $context->apply_parameter_debug_filters( 'body', { foo=>1, bar=>2, baz=>3 } ); +is_deeply( $params, { foo => $filter_str, bar => $filter_str, baz => 3 }, 'using the "all" type filter on body params' ); +$params = $context->apply_parameter_debug_filters( 'query', { foo=>1, bar=>2, baz=>3 } ); +is_deeply( $params, { foo => $filter_str, bar => $filter_str, baz => 3 }, 'using the "all" type filter on query params' ); -- 1.6.2.4