Skip to content

Commit d10ab92

Browse files
committed
Merge branch 'censor'
2 parents 6f5167c + 32d062b commit d10ab92

File tree

4 files changed

+161
-36
lines changed

4 files changed

+161
-36
lines changed

Changes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
* GH #1701: Split cookie values on & only (Yanick Champoux)
55

66
[ ENHANCEMENTS ]
7+
* GH #530: Make data censoring configurable (Yanick Champoux, David
8+
Precious)
79
* GH #1723: Enable use of a different Template Toolkit base class
810
(Andy Beverley)
911
* PR #1727: Don't create CPAN package files when generating new apps

cpanfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ requires 'Attribute::Handlers';
33
requires 'Carp';
44
requires 'Clone';
55
requires 'Config::Any';
6+
requires 'Data::Censor' => '0.04';
67
requires 'Digest::SHA';
78
requires 'Encode';
89
requires 'Exporter', '5.57';

lib/Dancer2/Core/Error.pm

Lines changed: 104 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use Dancer2::Core::HTTP;
88
use Data::Dumper;
99
use Dancer2::FileUtils qw/path open_file/;
1010
use Sub::Quote;
11-
use Module::Runtime 'require_module';
11+
use Module::Runtime qw/ require_module use_module /;
1212
use Ref::Util qw< is_hashref >;
1313
use Clone qw(clone);
1414

@@ -48,6 +48,52 @@ has title => (
4848
builder => '_build_title',
4949
);
5050

51+
has censor => (
52+
is => 'ro',
53+
isa => CodeRef,
54+
lazy => 1,
55+
default => sub {
56+
my $self = shift;
57+
58+
if( my $custom = $self->has_app && $self->app->setting('error_censor') ) {
59+
60+
if( is_hashref $custom ) {
61+
die "only one key can be set for the 'error_censor' setting\n"
62+
if 1 != keys %$custom;
63+
64+
my( $class, $args ) = %$custom;
65+
66+
my $censor = use_module($class)->new(%$args);
67+
68+
return sub {
69+
$censor->censor(@_);
70+
}
71+
}
72+
73+
my $coderef = eval '\&'.$custom;
74+
75+
# it's already defined? Nice! We're done
76+
return $coderef if $coderef;
77+
78+
my $module = $custom =~ s/::[^:]*?$//r;
79+
80+
require_module($module);
81+
82+
return eval '\&'.$custom;
83+
}
84+
85+
# reminder: update POD below if changing the config here
86+
my $data_censor = use_module('Data::Censor')->new(
87+
sensitive_fields => qr/pass|card.?num|pan|secret/i,
88+
replacement => "Hidden (looks potentially sensitive)",
89+
);
90+
91+
return sub {
92+
$data_censor->censor(@_);
93+
};
94+
}
95+
);
96+
5197
sub _build_title {
5298
my ($self) = @_;
5399
my $title = 'Error ' . $self->status;
@@ -367,11 +413,11 @@ sub backtrace {
367413
}
368414

369415
sub dumper {
370-
my $obj = shift;
416+
my ($self,$obj) = @_;
371417

372418
# Take a copy of the data, so we can mask sensitive-looking stuff:
373419
my $data = clone($obj);
374-
my $censored = _censor( $data );
420+
my $censored = $self->censor->( $data );
375421

376422
#use Data::Dumper;
377423
my $dd = Data::Dumper->new( [ $data ] );
@@ -399,7 +445,7 @@ sub environment {
399445
my $env = $self->has_app && $self->app->has_request && $self->app->request->env;
400446

401447
# Get a sanitised dump of the settings, session and environment
402-
$_ = $_ ? dumper($_) : '<i>undefined</i>' for $settings, $session, $env;
448+
$_ = $_ ? $self->dumper($_) : '<i>undefined</i>' for $settings, $session, $env;
403449

404450
return <<"END_HTML";
405451
<div class="title">Stack</div><pre class="content">$stack</pre>
@@ -423,37 +469,6 @@ sub get_caller {
423469

424470
# private
425471

426-
# Given a hashref, censor anything that looks sensitive. Returns number of
427-
# items which were "censored".
428-
429-
sub _censor {
430-
my $hash = shift;
431-
my $visited = shift || {};
432-
433-
unless ( $hash && is_hashref($hash) ) {
434-
carp "_censor given incorrect input: $hash";
435-
return;
436-
}
437-
438-
my $censored = 0;
439-
for my $key ( keys %$hash ) {
440-
if ( is_hashref( $hash->{$key} ) ) {
441-
if (!$visited->{ $hash->{$key} }) {
442-
# mark the new ref as visited
443-
$visited->{ $hash->{$key} } = 1;
444-
445-
$censored += _censor( $hash->{$key}, $visited );
446-
}
447-
}
448-
elsif ( $key =~ /(pass|card?num|pan|secret)/i ) {
449-
$hash->{$key} = "Hidden (looks potentially sensitive)";
450-
$censored++;
451-
}
452-
}
453-
454-
return $censored;
455-
}
456-
457472
# Replaces the entities that are illegal in (X)HTML.
458473
sub _html_encode {
459474
my $value = shift;
@@ -523,6 +538,60 @@ This is only an attribute getter, you'll have to set it at C<new>.
523538
524539
The message of the error page.
525540
541+
=attr censor
542+
543+
The function to use to censor error messages. By default it uses the C<censor> method of L<Data::Censor>"
544+
545+
# default censor function used by `error_censor`
546+
# is equivalent to
547+
sub MyApp::censor {
548+
Data::Censor->new(
549+
sensitive_fields => qr/pass|card.?num|pan|secret/i,
550+
replacement => "Hidden (looks potentially sensitive)",
551+
)->censor(@_);
552+
}
553+
setting error_censor => 'MyApp::censor';
554+
555+
It can be configured via the app setting C<error_censor>. If provided,
556+
C<error_censor> has to be the fully qualified name of the censor
557+
function. That function is expected to take in the data as a hashref,
558+
modify it in place and return the number of items 'censored'.
559+
560+
For example, using L<Data::Censor>.
561+
562+
# in config.yml
563+
error_censor: MyApp::Censor::censor
564+
565+
# in MyApp::Censor
566+
package MyApp::Censor;
567+
568+
use Data::Censor;
569+
570+
my $data_censor = Data::Censor->new(
571+
sensitive_fields => [ qw(card_number password hush) ],
572+
replacement => '(Sensitive data hidden)',
573+
);
574+
575+
sub censor { $data_censor->censor(@_) }
576+
577+
1;
578+
579+
580+
As a shortcut, C<error_censor> can also be the key/value combo of
581+
a class and the arguments for its constructor. The created object
582+
is expected to have a method C<censor>. For example, the use of
583+
L<Data::Censor> above could also have been done via the config
584+
585+
error_censor:
586+
Data::Censor:
587+
sensitive_fields:
588+
- card_number
589+
- password
590+
- hush
591+
replacement: '(Sensitive data hidden)'
592+
593+
594+
526595
=method throw($response)
527596
528597
Populates the content of the response with the error's information.

t/error.t

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
use strict;
22
use warnings;
3+
4+
use lib 't/lib';
5+
36
use Test::More import => ['!pass'];
47
use Plack::Test;
58
use HTTP::Request::Common;
69
use Ref::Util qw<is_coderef>;
710
use List::Util qw<all>;
11+
use Module::Runtime qw/ require_module /;
812

913
use Dancer2::Core::App;
1014
use Dancer2::Core::Response;
@@ -37,6 +41,7 @@ my $request = $app->build_request($env);
3741

3842
$app->set_request($request);
3943

44+
4045
subtest 'basic defaults of Error object' => sub {
4146
my $err = Dancer2::Core::Error->new( app => $app );
4247
is $err->status, 500, 'code';
@@ -240,8 +245,56 @@ subtest 'Errors with show_stacktrace and circular references' => sub {
240245
'Values for other keys (non-sensitive) appear in the stacktrace');
241246
};
242247

243-
done_testing;
248+
subtest censor => sub {
249+
sub MyApp::Censor::censor { $_[0]->{hush} = 'NOT TELLING'; return 1; }
250+
251+
my $app = Dancer2::Core::App->new( name => 'main' );
252+
253+
$app->setting( password => 'potato' ); # oh my, we're leaking a password
254+
255+
subtest 'core censor()' => sub {
256+
my $error = Dancer2::Core::Error->new( app => $app );
257+
258+
unlike $error->environment => qr/potato/, 'the password is censored';
259+
like $error->environment => qr/^.*password.*Hidden.*$/m, 'we say it is hidden';
260+
};
261+
262+
subtest 'custom censor' => sub {
263+
264+
subtest 'via function string' => sub {
265+
my $app = Dancer2::Core::App->new( name => 'main' );
266+
my $error = Dancer2::Core::Error->new( app => $app );
267+
268+
$app->setting( hush => 'potato' );
269+
270+
$app->setting( error_censor => 'MyApp::Censor::censor' );
244271

272+
unlike $error->environment => qr/potato/, 'the password is censored';
273+
like $error->environment => qr/^ .* hush .* NOT \s TELLING .* $/xm, 'we say it is hidden';
274+
};
275+
276+
subtest 'via class hashref' => sub {
277+
my $app = Dancer2::Core::App->new( name => 'main' );
278+
$app->setting( 'error_censor' => {
279+
'Data::Censor' => {
280+
sensitive_fields => ['hush'],
281+
replacement => 'NOT TELLING',
282+
}
283+
});
284+
285+
my $error = Dancer2::Core::Error->new( app => $app );
286+
287+
$app->setting( hush => 'potato' );
288+
289+
unlike $error->environment => qr/potato/, 'the password is censored';
290+
like $error->environment => qr/^ .* hush .* NOT \s TELLING .* $/xm, 'we say it is hidden';
291+
};
292+
293+
}
294+
};
295+
296+
297+
done_testing;
245298

246299
{ # Simple test exception class
247300
package MyTestException;

0 commit comments

Comments
 (0)