File Coverage

blib/lib/PAGI/Middleware/SecurityHeaders.pm
Criterion Covered Total %
statement 48 51 94.1
branch 23 28 82.1
condition 0 2 0.0
subroutine 8 8 100.0
pod 1 1 100.0
total 80 90 88.8


line stmt bran cond sub pod time code
1             package PAGI::Middleware::SecurityHeaders;
2              
3 2     2   469 use strict;
  2         2  
  2         65  
4 2     2   6 use warnings;
  2         3  
  2         95  
5 2     2   8 use parent 'PAGI::Middleware';
  2         3  
  2         13  
6 2     2   97 use Future::AsyncAwait;
  2         3  
  2         79  
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 10     10   20 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 10 100       44 ? $config->{x_frame_options} : 'SAMEORIGIN';
73             $self->{x_content_type_options} = exists $config->{x_content_type_options}
74 10 100       31 ? $config->{x_content_type_options} : 'nosniff';
75             $self->{x_xss_protection} = exists $config->{x_xss_protection}
76 10 100       27 ? $config->{x_xss_protection} : '1; mode=block';
77             $self->{referrer_policy} = exists $config->{referrer_policy}
78 10 100       50 ? $config->{referrer_policy} : 'strict-origin-when-cross-origin';
79 10         15 $self->{strict_transport_security} = $config->{strict_transport_security};
80 10         23 $self->{content_security_policy} = $config->{content_security_policy};
81 10         27 $self->{permissions_policy} = $config->{permissions_policy};
82             }
83              
84             sub wrap {
85 10     10 1 34 my ($self, $app) = @_;
86              
87 17     17   65 return async sub {
88 17         30 my ($scope, $receive, $send) = @_;
89             # Only handle HTTP requests
90 17 100       51 if ($scope->{type} ne 'http') {
91 7         21 await $app->($scope, $receive, $send);
92 7         578 return;
93             }
94              
95             # Intercept send to add security headers
96 20         770 my $wrapped_send = async sub {
97 20         29 my ($event) = @_;
98 20 100       44 if ($event->{type} eq 'http.response.start') {
99 10         37 $self->_add_security_headers($event->{headers}, $scope);
100             }
101 20         39 await $send->($event);
102 10         44 };
103              
104 10         39 await $app->($scope, $receive, $wrapped_send);
105 10         50 };
106             }
107              
108             sub _add_security_headers {
109 10     10   17 my ($self, $headers, $scope) = @_;
110              
111             # X-Frame-Options
112 10 100       24 if (defined $self->{x_frame_options}) {
113 9         32 push @$headers, ['X-Frame-Options', $self->{x_frame_options}];
114             }
115              
116             # X-Content-Type-Options
117 10 50       87 if (defined $self->{x_content_type_options}) {
118 10         32 push @$headers, ['X-Content-Type-Options', $self->{x_content_type_options}];
119             }
120              
121             # X-XSS-Protection
122 10 100       31 if (defined $self->{x_xss_protection}) {
123 9         26 push @$headers, ['X-XSS-Protection', $self->{x_xss_protection}];
124             }
125              
126             # Referrer-Policy
127 10 50       23 if (defined $self->{referrer_policy}) {
128 10         28 push @$headers, ['Referrer-Policy', $self->{referrer_policy}];
129             }
130              
131             # Strict-Transport-Security (only for HTTPS)
132 10 50       28 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 10 100       44 if (defined $self->{content_security_policy}) {
141 2         6 push @$headers, ['Content-Security-Policy', $self->{content_security_policy}];
142             }
143              
144             # Permissions-Policy
145 10 100       26 if (defined $self->{permissions_policy}) {
146 2         8 push @$headers, ['Permissions-Policy', $self->{permissions_policy}];
147             }
148             }
149              
150             1;
151              
152             __END__