File Coverage

lib/PAGI/Middleware/SecurityHeaders.pm
Criterion Covered Total %
statement 44 51 86.2
branch 15 28 53.5
condition 0 2 0.0
subroutine 8 8 100.0
pod 1 1 100.0
total 68 90 75.5


line stmt bran cond sub pod time code
1             package PAGI::Middleware::SecurityHeaders;
2             $PAGI::Middleware::SecurityHeaders::VERSION = '0.002000';
3 1     1   483 use strict;
  1         2  
  1         31  
4 1     1   3 use warnings;
  1         2  
  1         36  
5 1     1   3 use parent 'PAGI::Middleware';
  1         2  
  1         4  
6 1     1   46 use Future::AsyncAwait;
  1         1  
  1         9  
7              
8             =head1 NAME
9              
10             PAGI::Middleware::SecurityHeaders - Security headers middleware
11              
12             =head1 SYNOPSIS
13              
14             use PAGI::Middleware::Builder;
15              
16             my $app = builder {
17             enable 'SecurityHeaders',
18             x_frame_options => 'DENY',
19             x_content_type_options => 'nosniff',
20             x_xss_protection => '1; mode=block',
21             strict_transport_security => 'max-age=31536000; includeSubDomains';
22             $my_app;
23             };
24              
25             =head1 DESCRIPTION
26              
27             PAGI::Middleware::SecurityHeaders adds common security-related HTTP headers
28             to responses. These headers help protect against various web vulnerabilities.
29              
30             =head1 CONFIGURATION
31              
32             =over 4
33              
34             =item * x_frame_options (default: 'SAMEORIGIN')
35              
36             Controls whether the page can be displayed in a frame.
37             Values: 'DENY', 'SAMEORIGIN', or 'ALLOW-FROM uri'.
38              
39             =item * x_content_type_options (default: 'nosniff')
40              
41             Prevents MIME type sniffing.
42              
43             =item * x_xss_protection (default: '1; mode=block')
44              
45             Enables XSS filter in browsers.
46              
47             =item * referrer_policy (default: 'strict-origin-when-cross-origin')
48              
49             Controls the Referer header.
50              
51             =item * strict_transport_security (default: undef)
52              
53             HSTS header. Set to enable HTTPS enforcement.
54              
55             =item * content_security_policy (default: undef)
56              
57             CSP header. Set to define content security policy.
58              
59             =item * permissions_policy (default: undef)
60              
61             Permissions-Policy header for feature control.
62              
63             =back
64              
65             =cut
66              
67             sub _init {
68 3     3   5 my ($self, $config) = @_;
69              
70             # Use exists() to allow explicitly passing undef to disable a header
71             $self->{x_frame_options} = exists $config->{x_frame_options}
72 3 100       13 ? $config->{x_frame_options} : 'SAMEORIGIN';
73             $self->{x_content_type_options} = exists $config->{x_content_type_options}
74 3 50       7 ? $config->{x_content_type_options} : 'nosniff';
75             $self->{x_xss_protection} = exists $config->{x_xss_protection}
76 3 50       8 ? $config->{x_xss_protection} : '1; mode=block';
77             $self->{referrer_policy} = exists $config->{referrer_policy}
78 3 50       5 ? $config->{referrer_policy} : 'strict-origin-when-cross-origin';
79 3         7 $self->{strict_transport_security} = $config->{strict_transport_security};
80 3         6 $self->{content_security_policy} = $config->{content_security_policy};
81 3         6 $self->{permissions_policy} = $config->{permissions_policy};
82             }
83              
84             sub wrap {
85 3     3 1 20 my ($self, $app) = @_;
86              
87 3     3   63 return async sub {
88 3         39 my ($scope, $receive, $send) = @_;
89             # Only handle HTTP requests
90 3 50       24 if ($scope->{type} ne 'http') {
91 0         0 await $app->($scope, $receive, $send);
92 0         0 return;
93             }
94              
95             # Intercept send to add security headers
96 6         237 my $wrapped_send = async sub {
97 6         8 my ($event) = @_;
98 6 100       15 if ($event->{type} eq 'http.response.start') {
99 3         8 $self->_add_security_headers($event->{headers}, $scope);
100             }
101 6         10 await $send->($event);
102 3         9 };
103              
104 3         7 await $app->($scope, $receive, $wrapped_send);
105 3         13 };
106             }
107              
108             sub _add_security_headers {
109 3     3   3 my ($self, $headers, $scope) = @_;
110              
111             # X-Frame-Options
112 3 50       7 if (defined $self->{x_frame_options}) {
113 3         6 push @$headers, ['X-Frame-Options', $self->{x_frame_options}];
114             }
115              
116             # X-Content-Type-Options
117 3 50       8 if (defined $self->{x_content_type_options}) {
118 3         5 push @$headers, ['X-Content-Type-Options', $self->{x_content_type_options}];
119             }
120              
121             # X-XSS-Protection
122 3 50       5 if (defined $self->{x_xss_protection}) {
123 3         6 push @$headers, ['X-XSS-Protection', $self->{x_xss_protection}];
124             }
125              
126             # Referrer-Policy
127 3 50       4 if (defined $self->{referrer_policy}) {
128 3         5 push @$headers, ['Referrer-Policy', $self->{referrer_policy}];
129             }
130              
131             # Strict-Transport-Security (only for HTTPS)
132 3 50       6 if (defined $self->{strict_transport_security}) {
133 0   0     0 my $scheme = $scope->{scheme} // 'http';
134 0 0       0 if ($scheme eq 'https') {
135 0         0 push @$headers, ['Strict-Transport-Security', $self->{strict_transport_security}];
136             }
137             }
138              
139             # Content-Security-Policy
140 3 50       5 if (defined $self->{content_security_policy}) {
141 0         0 push @$headers, ['Content-Security-Policy', $self->{content_security_policy}];
142             }
143              
144             # Permissions-Policy
145 3 50       7 if (defined $self->{permissions_policy}) {
146 0           push @$headers, ['Permissions-Policy', $self->{permissions_policy}];
147             }
148             }
149              
150             1;
151              
152             __END__