File Coverage

blib/lib/Mojolicious/Plugin/SecurityHeader.pm
Criterion Covered Total %
statement 112 112 100.0
branch 98 98 100.0
condition 27 27 100.0
subroutine 11 11 100.0
pod 1 1 100.0
total 249 249 100.0


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::SecurityHeader;
2              
3             # ABSTRACT: Mojolicious Plugin
4              
5 44     44   779325 use Mojo::Base 'Mojolicious::Plugin';
  44         387348  
  44         334  
6              
7             our $VERSION = '0.07';
8              
9             sub register {
10 66     66 1 154180 my ($self, $app, $headers) = @_;
11              
12 66 100       316 return if !$headers;
13 64 100       235 return if !ref $headers;
14 63 100       263 return if 'ARRAY' ne ref $headers;
15              
16 62         268 my @headers_list = qw(
17             Strict-Transport-Security Referrer-Policy
18             X-Content-Type-Options X-Frame-Options X-Xss-Protection
19             Content-Security-Policy Access-Control-Allow-Origin
20             Access-Control-Expose-Headers Access-Control-Max-Age
21             Access-Control-Allow-Credentials Access-Control-Allow-Methods
22             Access-Control-Allow-Headers
23             );
24              
25 62         126 my %valid_headers;
26 62         625 @valid_headers{@headers_list} = (1) x @headers_list;
27              
28 62         561 my %values = (
29             'X-Content-Type-Options' => 'nosniff',
30             'X-Xss-Protection' => \&_check_xp,
31             'X-Frame-Options' => \&_check_fo,
32             'Content-Security-Policy' => \&_check_csp,
33             'Access-Control-Allow-Methods' => \&_check_methods,
34             'Access-Control-Allow-Origin' => \&_is_url,
35             'Access-Control-Allow-Headers' => \&_check_list,
36             'Access-Control-Expose-Headers' => \&_check_list,
37             'Access-Control-Max-Age' => \&_is_int,
38             'Access-Control-Allow-Credentials' => 'true',
39             'Strict-Transport-Security' => \&_check_sts,
40             'Referrer-Policy' => [
41             "",
42             "no-referrer",
43             "no-referrer-when-downgrade",
44             "same-origin",
45             "origin",
46             "strict-origin",
47             "origin-when-cross-origin",
48             "strict-origin-when-cross-origin",
49             "unsafe-url"
50             ],
51             );
52              
53 62         253 my %options = (
54             'Strict-Transport-Security' => { includeSubDomains => 1, preload => 1 },
55             );
56              
57 62         321 my %headers_default = (
58             'Referrer-Policy' => "",
59             'Strict-Transport-Security' => "max-age=31536000",
60             'X-Content-Type-Options' => "nosniff",
61             'X-Xss-Protection' => '1; mode=block',
62             'X-Frame-Options' => 'DENY',
63             'Content-Security-Policy' => "default-src 'self'",
64             'Access-Control-Allow-Origin' => '*',
65             'Access-Control-Allow-Credentials' => 'true',
66             );
67              
68 62         211 my %security_headers;
69              
70             my $last_header;
71 62         0 my $header_value;
72              
73             HEADER:
74 62         120 for my $header ( @{ $headers } ) {
  62         182  
75 117 100       336 next HEADER if !defined $header;
76              
77 116 100       488 if ( $valid_headers{$header} ) {
    100          
78 64 100       182 if ( $last_header ) {
79 3   100     14 $security_headers{$last_header} = $header_value // $headers_default{$last_header};
80             }
81              
82 64         131 undef $header_value;
83 64         149 $last_header = $header;
84             }
85             elsif ( $last_header ) {
86 51         143 $header_value = $header;
87              
88 51         149 my $ref = ref $values{$last_header};
89              
90 51 100       176 if ( $ref eq 'CODE' ) {
    100          
91 37         159 $header_value = $values{$last_header}->($header_value, $options{$last_header});
92              
93 37 100       184 undef $last_header if !defined $header_value;
94             }
95             elsif ( $ref eq 'ARRAY' ) {
96 9         13 ($header_value) = grep{ $header_value eq $_ }@{ $values{$last_header} };
  81         160  
  9         26  
97              
98 9 100       35 undef $last_header if !$header_value;
99             }
100             else {
101 5 100       29 undef $last_header if $header_value ne $values{$last_header};
102             }
103             }
104             }
105              
106 62 100 100     336 $security_headers{$last_header} = $header_value // $headers_default{$last_header} if $last_header;
107              
108             $app->hook( before_dispatch => sub {
109 116     116   631286 my $c = shift;
110              
111             HEADER_NAME:
112 116         422 for my $header_name ( keys %security_headers ) {
113 108 100       578 next HEADER_NAME if !defined $security_headers{$header_name};
114 104         435 $c->res->headers->header( $header_name => $security_headers{$header_name} );
115             }
116 62         619 });
117             }
118              
119             sub _is_int {
120 10     10   3015 my ($value, $options) = @_;
121              
122 10 100       34 return if !defined $value;
123 9 100       25 return if ref $value;
124 7 100       68 return if $value !~ m{\A[0-9]+\z};
125 4         45 return $value;
126             }
127              
128             sub _check_methods {
129 17     17   5821 my ($value, $options) = @_;
130              
131 17 100       54 return if !defined $value;
132              
133 16         62 my @methods = qw(GET DELETE POST PATCH OPTIONS HEAD CONNECT TRACE PUT);
134 16 100 100     68 if ( !ref $value && $value eq '*' ) {
135 1         7 return join ', ', @methods;
136             }
137              
138 15 100       46 return uc $value if !ref $value;
139 11 100       33 return if 'ARRAY' ne ref $value;
140              
141 10         24 my %allowed = map{ $_ => 1 }@methods;
  90         208  
142 10 100 100     30 my $return = join ', ', map{ defined $_ && $allowed{uc $_} ? uc $_ : () }@{$value};
  16         89  
  10         24  
143              
144 10   100     59 return $return || undef;
145             }
146              
147             sub _check_list {
148 15     15   4500 my ($value, $options) = @_;
149              
150 15 100       52 return if !defined $value;
151 14 100       44 return $value if !ref $value;
152 9 100       70 return if 'ARRAY' ne ref $value;
153              
154 7         12 my $return = join ', ', @{$value};
  7         27  
155              
156 7   100     32 return $return || undef;
157             }
158              
159             sub _is_url {
160 10     10   3115 my ($value, $options) = @_;
161              
162 10 100       37 return if !defined $value;
163 9 100       27 return if ref $value;
164 7 100       24 return '*' if $value eq '*';
165              
166 5 100       25 return $value if $value =~ m{\Ahttps?://\S+\z}xms;
167 3         6 return;
168             }
169              
170             sub _check_csp {
171 12     12   3664 my ($value, $options) = @_;
172              
173 12         25 my $option = '';
174              
175 12 100       47 return $option if !ref $value;
176 9 100       31 return $option if 'HASH' ne ref $value;
177              
178 7         15 for my $key ( reverse sort keys %{ $value } ) {
  7         43  
179 10         29 my $tmp_value = $value->{$key};
180 10         46 $option .= sprintf "%s-src %s; ", $key, $tmp_value;
181             }
182              
183 7         20 return $option;
184             }
185              
186             sub _check_sts {
187 8     8   533 my ($value, $options) = @_;
188              
189 8         20 my $option = '';
190              
191 8 100       35 if ( ref $value ) {
192 3         9 $option = $value->{opt};
193 3         6 $value = $value->{maxage};
194              
195 3 100       11 $option = '' if !$options->{$option};
196             }
197              
198 8 100       26 $option = '; ' . $option if $option;
199              
200 8 100       34 return 'max-age=31536000' . $option if $value == -1;
201 6 100       21 return if $value < 0;
202 5 100       21 return if $value ne int $value;
203 4         31 return 'max-age=' . $value . $option;
204             }
205              
206             sub _check_fo {
207 14     14   4473 my ($value) = @_;
208              
209 14         46 my %allowed = ('DENY' => 1, 'SAMEORIGIN' => 1);
210            
211 14 100       47 return 'DENY' if !defined $value;
212 13 100       49 return $value if $allowed{$value};
213 9 100       30 return if !ref $value;
214 7 100       23 return if 'HASH' ne ref $value;
215              
216 6 100       25 return if !$value->{'ALLOW-FROM'};
217 3         17 return 'ALLOW-FROM ' . $value->{'ALLOW-FROM'};
218             }
219              
220             sub _check_xp {
221 17     17   5745 my ($value, $options) = @_;
222              
223 17 100       60 if ( !ref $value ) {
224 6   100     21 $value //= '';
225              
226 6 100 100     37 return if $value ne '1' && $value ne '0';
227 3         9 return $value;
228             }
229              
230 11 100       35 return if 'HASH' ne ref $value;
231 10 100 100     67 return if !exists $value->{value} || $value->{value} ne '1';
232              
233 7         17 my $option = '';
234              
235 7 100 100     39 if ( $value->{mode} && $value->{mode} eq 'block' ) {
    100          
236 2         6 $option = 'mode=block';
237             }
238             elsif ( $value->{report} ) {
239 2         8 $option = 'report=' . $value->{report};
240             }
241              
242 7         16 $value = '1; ';
243 7 100       19 $value .= $option if $option;
244              
245 7         19 return $value;
246             }
247              
248             1;
249              
250             __END__