Skip to content

Commit e9be21c

Browse files
committed
Improved header generation and added caching
1 parent 650a705 commit e9be21c

5 files changed

Lines changed: 310 additions & 207 deletions

File tree

classes/CSPHeaderBuilder.php

Lines changed: 0 additions & 122 deletions
This file was deleted.

classes/HeaderBuilder.php

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
<?php
2+
3+
namespace Zaxbux\SecurityHeaders\Classes;
4+
5+
use Cache;
6+
use Illuminate\Http\Response;
7+
use Zaxbux\SecurityHeaders\Models\Settings;
8+
use Zaxbux\SecurityHeaders\Classes\HttpHeader;
9+
10+
class HeaderBuilder {
11+
12+
const CACHE_KEY_CONTENT_SECURITY_POLICY = "zaxbux_securityheaders_csp";
13+
const CACHE_KEY_STRICT_TRANSPORT_SECURITY = "zaxbux_securityheaders_hsts";
14+
const CACHE_KEY_REFERRER_POLICY = "zaxbux_securityheaders_ref_policy";
15+
const CACHE_KEY_FRAME_OPTIONS = "zaxbux_securityheaders_frame_options";
16+
const CACHE_KEY_CONTENT_TYPE_OPTIONS = "zaxbux_securityheaders_content_type";
17+
const CACHE_KEY_XSS_PROTECTION = "zaxbux_securityheaders_xss";
18+
19+
20+
/**
21+
* Add the Content-Security-Policy or Content-Security-Policy-Report-Only header to the response
22+
*
23+
* @param Illuminate\Http\Response
24+
*/
25+
public static function addContentSecurityPolicy(Response $response, $nonce) {
26+
$header = Cache::rememberForever(self::CACHE_KEY_CONTENT_SECURITY_POLICY, function() {
27+
$policy = Settings::get('csp');
28+
29+
if (!$policy['enabled']) {
30+
return false;
31+
}
32+
33+
return self::buildContentSecurityPolicyHeader($policy);
34+
});
35+
36+
if ($header) {
37+
$response->header($header->getName(), \sprintf($header->getValue(), $nonce));
38+
}
39+
}
40+
41+
/**
42+
* Add the Strict-Transport-Security header to the response
43+
*
44+
* @param Illuminate\Http\Response
45+
*/
46+
public static function addStrictTransportSecurity(Response $response) {
47+
$header = Cache::rememberForever(self::CACHE_KEY_STRICT_TRANSPORT_SECURITY, function() {
48+
if (!Settings::get('hsts_enable')) {
49+
return false;
50+
}
51+
52+
$value = sprintf('max-age=%d', Settings::get('hsts_max_age'));
53+
54+
if (Settings::get('hsts_subdomains')) {
55+
$value .= '; includeSubDomains';
56+
}
57+
58+
if (Settings::get('hsts_preload')) {
59+
$value .= '; preload';
60+
}
61+
62+
return new HttpHeader('Strict-Transport-Security', $value);
63+
});
64+
65+
if ($header) {
66+
$response->header($header->getName(), $header->getValue());
67+
}
68+
}
69+
70+
/**
71+
* Add the Referrer-Policy header to the response
72+
*
73+
* @param Illuminate\Http\Response
74+
*/
75+
public static function addReferrerPolicy(Response $response) {
76+
$header = Cache::rememberForever(self::CACHE_KEY_REFERRER_POLICY, function() {
77+
if ($value = Settings::get('referrer_policy')) {
78+
return new HttpHeader('Referrer-Policy', $value);
79+
}
80+
81+
return false;
82+
});
83+
84+
if ($header) {
85+
$response->header($header->getName(), $header->getValue());
86+
}
87+
}
88+
89+
/**
90+
* Add the Frame-options header to the response
91+
*
92+
* @param Illuminate\Http\Response
93+
*/
94+
public static function addFrameOptions(Response $response) {
95+
$header = Cache::rememberForever(self::CACHE_KEY_FRAME_OPTIONS, function() {
96+
if (Settings::get('frame_options')) {
97+
return new HttpHeader('X-Frame-Options', 'nosniff');
98+
}
99+
100+
return false;
101+
});
102+
103+
if ($header) {
104+
$response->header($header->getName(), $header->getValue());
105+
}
106+
}
107+
108+
/**
109+
* Add the X-Content-Type-Options header to the response
110+
*
111+
* @param Illuminate\Http\Response
112+
*/
113+
public static function addContentTypeOptions(Response $response) {
114+
$header = Cache::rememberForever(self::CACHE_KEY_CONTENT_TYPE_OPTIONS, function() {
115+
if ($value = Settings::get('content_type_options')) {
116+
return new HttpHeader('X-Content-Type-Options', $value);
117+
}
118+
119+
return false;
120+
});
121+
122+
if ($header) {
123+
$response->header($header->getName(), $header->getValue());
124+
}
125+
}
126+
127+
/**
128+
* Add the X-XSS-Protection header to the response
129+
*
130+
* @param Illuminate\Http\Response
131+
*/
132+
public static function addXSSProtection(Response $response) {
133+
$header = Cache::rememberForever(self::CACHE_KEY_XSS_PROTECTION, function() {
134+
$value = Settings::get('xss_protection');
135+
136+
switch ($value) {
137+
case 'disable':
138+
$value = '0';
139+
break;
140+
case 'enable':
141+
$value = '1';
142+
break;
143+
case 'block':
144+
$value = '1; mode=block';
145+
break;
146+
default:
147+
return false;
148+
}
149+
150+
return new HttpHeader('X-XSS-Protection', $value);
151+
});
152+
153+
if ($header) {
154+
$response->header($header->getName(), $header->getValue());
155+
}
156+
}
157+
158+
private static function buildContentSecurityPolicyHeader($policy) {
159+
$header = new HttpHeader('Content-Security-Policy');
160+
161+
$directives = [];
162+
163+
if ($policy['report-only']) {
164+
$header->setName('Content-Security-Policy-Report-Only');
165+
}
166+
167+
foreach ($policy as $directive => $value) {
168+
if (\in_array($directive, array_merge(Settings::CSP_FETCH_DIRECTIVES, Settings::CSP_NAVIGATION_DIRECTIVES, ['base-uri']))) {
169+
$directives[] = self::parseCSPDirectiveSources($directive, $value);
170+
}
171+
172+
if ($directive == 'plugin-types') {
173+
$types = [];
174+
175+
foreach ($value['types'] as $type) {
176+
$types[] = $type['value'];
177+
}
178+
179+
if (count($types) > 0) {
180+
$directives[] = sprintf('plugin-types %s;', \join(' ', $types));
181+
}
182+
}
183+
184+
if ($directive == 'sandbox' && $value) {
185+
$directives[] = \sprintf('sandbox %s;', $value);
186+
}
187+
188+
if ($directive == 'upgrade-insecure-requests' && $value == true) {
189+
$directives[] = 'upgrade-insecure-requests;';
190+
}
191+
192+
if ($directive == 'block-all-mixed-content' && $value == true) {
193+
$directives[] = 'block-all-mixed-content;';
194+
}
195+
}
196+
197+
if (count(array_filter($directives)) > 0) {
198+
return $header->setValue(\join(' ', $directives));
199+
}
200+
201+
return false;
202+
}
203+
204+
private static function parseCSPDirectiveSources($directive, $sourceData) {
205+
$sources = [];
206+
207+
foreach ($sourceData as $source => $data) {
208+
// User-provided URIs and hashes
209+
if ($source == '_sources') {
210+
foreach ($data as $value) {
211+
if (!empty($value['value'])) {
212+
$sources[] = $value['value'];
213+
}
214+
}
215+
216+
continue;
217+
}
218+
219+
if ($source == 'nonce' && $data == true) {
220+
// %1$s is replaced with the nonce on every response
221+
$sources[] = "'nonce-%1\$s'";
222+
223+
continue;
224+
}
225+
226+
// For checkboxes
227+
if ($data == true) {
228+
$sources[] = \sprintf("'%s'", $source);
229+
}
230+
}
231+
232+
if (count($sources) > 0) {
233+
return \sprintf('%s %s;', $directive, \join(' ', $sources));
234+
}
235+
}
236+
}

0 commit comments

Comments
 (0)