File Coverage

blib/lib/Mojolicious/Plugin/SecurityHeader.pm
Criterion Covered Total %
statement 110 110 100.0
branch 96 96 100.0
condition 24 24 100.0
subroutine 11 11 100.0
pod 1 1 100.0
total 242 242 100.0


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::SecurityHeader;
2              
3             # ABSTRACT: Mojolicious Plugin
4              
5 44     44   741848 use Mojo::Base 'Mojolicious::Plugin';
  44         385512  
  44         352  
6              
7             our $VERSION = '0.06';
8              
9             sub register {
10 65     65 1 156378 my ($self, $app, $headers) = @_;
11              
12 65 100       278 return if !$headers;
13 63 100       227 return if !ref $headers;
14 62 100       276 return if 'ARRAY' ne ref $headers;
15              
16 61         250 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 61         128 my %valid_headers;
26 61         629 @valid_headers{@headers_list} = (1) x @headers_list;
27              
28 61         531 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 61         254 my %options = (
54             'Strict-Transport-Security' => { includeSubDomains => 1, preload => 1 },
55             );
56              
57 61         342 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 61         201 my %security_headers;
69              
70             my $last_header;
71 61         0 my $header_value;
72              
73             HEADER:
74 61         121 for my $header ( @{ $headers } ) {
  61         176  
75 115 100       310 next HEADER if !defined $header;
76              
77 114 100       490 if ( $valid_headers{$header} ) {
    100          
78 63 100       196 if ( $last_header ) {
79 3   100     14 $security_headers{$last_header} = $header_value // $headers_default{$last_header};
80             }
81              
82 63         125 undef $header_value;
83 63         142 $last_header = $header;
84             }
85             elsif ( $last_header ) {
86 50         142 $header_value = $header;
87              
88 50         193 my $ref = ref $values{$last_header};
89              
90 50 100       166 if ( $ref eq 'CODE' ) {
    100          
91 36         143 $header_value = $values{$last_header}->($header_value, $options{$last_header});
92              
93 36 100       164 undef $last_header if !defined $header_value;
94             }
95             elsif ( $ref eq 'ARRAY' ) {
96 9         15 ($header_value) = grep{ $header_value eq $_ }@{ $values{$last_header} };
  81         164  
  9         22  
97              
98 9 100       33 undef $last_header if !$header_value;
99             }
100             else {
101 5 100       27 undef $last_header if $header_value ne $values{$last_header};
102             }
103             }
104             }
105              
106 61 100 100     321 $security_headers{$last_header} = $header_value // $headers_default{$last_header} if $last_header;
107              
108             $app->hook( before_dispatch => sub {
109 113     113   670042 my $c = shift;
110              
111             HEADER_NAME:
112 113         430 for my $header_name ( keys %security_headers ) {
113 105 100       628 next HEADER_NAME if !defined $security_headers{$header_name};
114 101         400 $c->res->headers->header( $header_name => $security_headers{$header_name} );
115             }
116 61         591 });
117             }
118              
119             sub _is_int {
120 10     10   3007 my ($value, $options) = @_;
121              
122 10 100       41 return if !defined $value;
123 9 100       27 return if ref $value;
124 7 100       42 return if $value !~ m{\A[0-9]+\z};
125 4         13 return $value;
126             }
127              
128             sub _check_methods {
129 16     16   5870 my ($value, $options) = @_;
130              
131 16 100       48 return if !defined $value;
132 15 100       47 return uc $value if !ref $value;
133 11 100       50 return if 'ARRAY' ne ref $value;
134              
135 10         35 my @methods = qw(GET DELETE POST PATCH OPTIONS HEAD CONNECT TRACE PUT);
136 10         23 my %allowed = map{ $_ => 1 }@methods;
  90         198  
137              
138 10 100 100     31 my $return = join ', ', map{ defined $_ && $allowed{uc $_} ? uc $_ : () }@{$value};
  16         113  
  10         27  
139              
140 10   100     58 return $return || undef;
141             }
142              
143             sub _check_list {
144 15     15   4265 my ($value, $options) = @_;
145              
146 15 100       49 return if !defined $value;
147 14 100       44 return $value if !ref $value;
148 9 100       67 return if 'ARRAY' ne ref $value;
149              
150 7         13 my $return = join ', ', @{$value};
  7         24  
151              
152 7   100     31 return $return || undef;
153             }
154              
155             sub _is_url {
156 10     10   3167 my ($value, $options) = @_;
157              
158 10 100       32 return if !defined $value;
159 9 100       25 return if ref $value;
160 7 100       22 return '*' if $value eq '*';
161              
162 5 100       29 return $value if $value =~ m{\Ahttps?://\S+\z}xms;
163 3         7 return;
164             }
165              
166             sub _check_csp {
167 12     12   3214 my ($value, $options) = @_;
168              
169 12         27 my $option = '';
170              
171 12 100       48 return $option if !ref $value;
172 9 100       32 return $option if 'HASH' ne ref $value;
173              
174 7         16 for my $key ( reverse sort keys %{ $value } ) {
  7         50  
175 10         25 my $tmp_value = $value->{$key};
176 10         52 $option .= sprintf "%s-src %s; ", $key, $tmp_value;
177             }
178              
179 7         23 return $option;
180             }
181              
182             sub _check_sts {
183 8     8   559 my ($value, $options) = @_;
184              
185 8         17 my $option = '';
186              
187 8 100       36 if ( ref $value ) {
188 3         7 $option = $value->{opt};
189 3         8 $value = $value->{maxage};
190              
191 3 100       12 $option = '' if !$options->{$option};
192             }
193              
194 8 100       29 $option = '; ' . $option if $option;
195              
196 8 100       32 return 'max-age=31536000' . $option if $value == -1;
197 6 100       22 return if $value < 0;
198 5 100       26 return if $value ne int $value;
199 4         65 return 'max-age=' . $value . $option;
200             }
201              
202             sub _check_fo {
203 14     14   4409 my ($value) = @_;
204              
205 14         47 my %allowed = ('DENY' => 1, 'SAMEORIGIN' => 1);
206            
207 14 100       47 return 'DENY' if !defined $value;
208 13 100       44 return $value if $allowed{$value};
209 9 100       33 return if !ref $value;
210 7 100       22 return if 'HASH' ne ref $value;
211              
212 6 100       24 return if !$value->{'ALLOW-FROM'};
213 3         16 return 'ALLOW-FROM ' . $value->{'ALLOW-FROM'};
214             }
215              
216             sub _check_xp {
217 17     17   5822 my ($value, $options) = @_;
218              
219 17 100       52 if ( !ref $value ) {
220 6   100     19 $value //= '';
221              
222 6 100 100     35 return if $value ne '1' && $value ne '0';
223 3         9 return $value;
224             }
225              
226 11 100       36 return if 'HASH' ne ref $value;
227 10 100 100     61 return if !exists $value->{value} || $value->{value} ne '1';
228              
229 7         18 my $option = '';
230              
231 7 100 100     40 if ( $value->{mode} && $value->{mode} eq 'block' ) {
    100          
232 2         8 $option = 'mode=block';
233             }
234             elsif ( $value->{report} ) {
235 2         8 $option = 'report=' . $value->{report};
236             }
237              
238 7         14 $value = '1; ';
239 7 100       18 $value .= $option if $option;
240              
241 7         19 return $value;
242             }
243              
244             1;
245              
246             __END__