File Coverage

blib/lib/Mojolicious/Plugin/SecurityHeader.pm
Criterion Covered Total %
statement 72 75 96.0
branch 47 60 78.3
condition 14 24 58.3
subroutine 7 8 87.5
pod 1 6 16.6
total 141 173 81.5


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::SecurityHeader;
2             # ABSTRACT: Mojolicious Plugin
3 23     23   17035 use Mojo::Base 'Mojolicious::Plugin';
  23         62  
  23         171  
4              
5             our $VERSION = '0.05';
6              
7             sub register {
8 38     38 1 104901 my ($self, $app, $headers) = @_;
9              
10 38 50       164 return if !ref $headers;
11 38 100       143 return if 'ARRAY' ne ref $headers;
12              
13 37         123 my @headers_list = qw(
14             Strict-Transport-Security Public-Key-Pins Referrer-Policy
15             X-Content-Type-Options X-Frame-Options X-Xss-Protection
16             Content-Security-Policy
17             );
18              
19 37         70 my %valid_headers;
20 37         269 @valid_headers{@headers_list} = (1) x @headers_list;
21              
22 37         245 my %values = (
23             'Strict-Transport-Security' => \&check_sts,
24             'Referrer-Policy' => [
25             "",
26             "no-referrer",
27             "no-referrer-when-downgrade",
28             "same-origin",
29             "origin",
30             "strict-origin",
31             "origin-when-cross-origin",
32             "strict-origin-when-cross-origin",
33             "unsafe-url"
34             ],
35             'X-Content-Type-Options' => ['nosniff'],
36             'X-Xss-Protection' => \&check_xp,
37             'X-Frame-Options' => \&check_fo,
38             'Content-Security-Policy' => \&check_csp,
39             );
40              
41 37         147 my %options = (
42             'Strict-Transport-Security' => { includeSubDomains => 1, preload => 1 },
43             );
44              
45 37         155 my %headers_default = (
46             'Referrer-Policy' => "",
47             'Strict-Transport-Security' => "max-age=31536000",
48             'X-Content-Type-Options' => "nosniff",
49             'X-Xss-Protection' => '1; mode=block',
50             'X-Frame-Options' => 'DENY',
51             'Content-Security-Policy' => "default-src 'self'",
52             );
53              
54 37         142 my %security_headers;
55              
56             my $last_header;
57 37         0 my $header_value;
58              
59             HEADER:
60 37 50       69 for my $header ( @{ $headers || [] } ) {
  37         140  
61 73 100       318 if ( $valid_headers{$header} ) {
    50          
62 40 100       111 if ( $last_header ) {
63 3   100     13 $security_headers{$last_header} = $header_value // $headers_default{$last_header};
64             }
65              
66 40         80 undef $header_value;
67 40         83 $last_header = $header;
68             }
69             elsif ( $last_header ) {
70 33         62 $header_value = $header;
71              
72 33 50       97 if ( $values{$last_header} ) {
73 33         78 my $ref = ref $values{$last_header};
74              
75 33 100       99 if ( $ref eq 'CODE' ) {
    50          
76 21   33     125 $header_value = $values{$last_header}->($header_value // $headers_default{$last_header}, $options{$last_header});
77             }
78             elsif ( $ref eq 'ARRAY' ) {
79 12         39 ($header_value) = grep{ $header_value eq $_ }@{ $values{$last_header} };
  84         175  
  12         36  
80              
81 12 100       50 undef $last_header if !$header_value;
82             }
83             }
84             }
85             }
86              
87 37 100 100     200 $security_headers{$last_header} = $header_value // $headers_default{$last_header} if $last_header;
88              
89             $app->hook( before_dispatch => sub {
90 82     82   360482 my $c = shift;
91              
92 82         347 for my $header_name ( keys %security_headers ) {
93 83         456 $c->res->headers->header( $header_name => $security_headers{$header_name} );
94             }
95 37         390 });
96             }
97              
98             sub check_csp {
99 6     6 0 18 my ($value, $options) = @_;
100              
101 6         12 my $option = '';
102              
103 6 100       16 if ( ref $value ) {
104 5 50       8 for my $key ( reverse sort keys %{ $value || {} } ) {
  5         31  
105 9         17 my $tmp_value = $value->{$key};
106 9         33 $option .= sprintf "%s-src %s; ", $key, $tmp_value;
107             }
108             }
109              
110 6         21 return $option;
111             }
112              
113             sub check_sts {
114 7     7 0 20 my ($value, $options) = @_;
115              
116 7         18 my $option = '';
117              
118 7 100       23 if ( ref $value ) {
119 3         8 $option = $value->{opt};
120 3         6 $value = $value->{maxage};
121              
122 3 100       9 $option = '' if !$options->{$option};
123             }
124              
125 7 100       20 $option = '; ' . $option if $option;
126              
127 7 100       29 return 'max-age=31536000' . $option if $value == -1;
128 5 50       14 return if $value < 0;
129 5 100       23 return if $value ne int $value;
130 4         17 return 'max-age=' . $value . $option;
131             }
132              
133             sub check_fo {
134 4     4 0 11 my ($value) = @_;
135              
136 4         14 my %allowed = ('DENY' => 1, 'SAMEORIGIN' => 1);
137            
138 4 50       12 return 'DENY' if !defined $value;
139 4 100       21 return $value if $allowed{$value};
140 2 100       11 return if !ref $value;
141              
142 1 50 33     8 return if ref $value && !$value->{'ALLOW-FROM'};
143 1         24 return 'ALLOW-FROM ' . $value->{'ALLOW-FROM'};
144             }
145              
146             sub check_xp {
147 4     4 0 16 my ($value, $options) = @_;
148              
149 4 100       14 if ( !ref $value ) {
150 2 100 66     15 return if $value ne '1' && $value ne '0';
151 1 50 33     8 return $value if $value eq '0' || $value eq '1';
152             }
153              
154 2 50 33     19 if ( ref $value && $value->{value} eq '1' ) {
155 2         7 my $option = '';
156              
157 2 100 66     30 if ( $value->{mode} && $value->{mode} eq 'block' ) {
    50          
158 1         4 $option = 'mode=block';
159             }
160             elsif ( $value->{report} ) {
161 1         4 $option = 'report=' . $value->{report};
162             }
163              
164 2 50       8 $value = '1; ' . $option if $option;
165 2         10 return $value;
166             }
167              
168 0           return;
169             }
170              
171             sub is_string {
172 0     0 0   my ($value) = @_;
173              
174 0           return defined $value;
175             }
176              
177             1;
178              
179             __END__