-
Notifications
You must be signed in to change notification settings - Fork 3.4k
/
SecurityHeadersMiddleware.php
262 lines (218 loc) · 8.27 KB
/
SecurityHeadersMiddleware.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @since 3.5.0
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Http\Middleware;
use InvalidArgumentException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
/**
* Handles common security headers in a convenient way
*
* @link https://book.cakephp.org/4/en/controllers/middleware.html#security-header-middleware
*/
class SecurityHeadersMiddleware implements MiddlewareInterface
{
/** @var string X-Content-Type-Option nosniff */
public const NOSNIFF = 'nosniff';
/** @var string X-Download-Option noopen */
public const NOOPEN = 'noopen';
/** @var string Referrer-Policy no-referrer */
public const NO_REFERRER = 'no-referrer';
/** @var string Referrer-Policy no-referrer-when-downgrade */
public const NO_REFERRER_WHEN_DOWNGRADE = 'no-referrer-when-downgrade';
/** @var string Referrer-Policy origin */
public const ORIGIN = 'origin';
/** @var string Referrer-Policy origin-when-cross-origin */
public const ORIGIN_WHEN_CROSS_ORIGIN = 'origin-when-cross-origin';
/** @var string Referrer-Policy same-origin */
public const SAME_ORIGIN = 'same-origin';
/** @var string Referrer-Policy strict-origin */
public const STRICT_ORIGIN = 'strict-origin';
/** @var string Referrer-Policy strict-origin-when-cross-origin */
public const STRICT_ORIGIN_WHEN_CROSS_ORIGIN = 'strict-origin-when-cross-origin';
/** @var string Referrer-Policy unsafe-url */
public const UNSAFE_URL = 'unsafe-url';
/** @var string X-Frame-Option deny */
public const DENY = 'deny';
/** @var string X-Frame-Option sameorigin */
public const SAMEORIGIN = 'sameorigin';
/** @var string X-Frame-Option allow-from */
public const ALLOW_FROM = 'allow-from';
/** @var string X-XSS-Protection block, sets enabled with block */
public const XSS_BLOCK = 'block';
/** @var string X-XSS-Protection enabled with block */
public const XSS_ENABLED_BLOCK = '1; mode=block';
/** @var string X-XSS-Protection enabled */
public const XSS_ENABLED = '1';
/** @var string X-XSS-Protection disabled */
public const XSS_DISABLED = '0';
/** @var string X-Permitted-Cross-Domain-Policy all */
public const ALL = 'all';
/** @var string X-Permitted-Cross-Domain-Policy none */
public const NONE = 'none';
/** @var string X-Permitted-Cross-Domain-Policy master-only */
public const MASTER_ONLY = 'master-only';
/** @var string X-Permitted-Cross-Domain-Policy by-content-type */
public const BY_CONTENT_TYPE = 'by-content-type';
/** @var string X-Permitted-Cross-Domain-Policy by-ftp-filename */
public const BY_FTP_FILENAME = 'by-ftp-filename';
/**
* Security related headers to set
*
* @var array
*/
protected $headers = [];
/**
* X-Content-Type-Options
*
* Sets the header value for it to 'nosniff'
*
* @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options
* @return $this
*/
public function noSniff()
{
$this->headers['x-content-type-options'] = self::NOSNIFF;
return $this;
}
/**
* X-Download-Options
*
* Sets the header value for it to 'noopen'
*
* @link https://msdn.microsoft.com/en-us/library/jj542450(v=vs.85).aspx
* @return $this
*/
public function noOpen()
{
$this->headers['x-download-options'] = self::NOOPEN;
return $this;
}
/**
* Referrer-Policy
*
* @link https://w3c.github.io/webappsec-referrer-policy
* @param string $policy Policy value. Available Value: 'no-referrer', 'no-referrer-when-downgrade', 'origin',
* 'origin-when-cross-origin', 'same-origin', 'strict-origin', 'strict-origin-when-cross-origin', 'unsafe-url'
* @return $this
*/
public function setReferrerPolicy(string $policy = self::SAME_ORIGIN)
{
$available = [
self::NO_REFERRER,
self::NO_REFERRER_WHEN_DOWNGRADE,
self::ORIGIN,
self::ORIGIN_WHEN_CROSS_ORIGIN,
self::SAME_ORIGIN,
self::STRICT_ORIGIN,
self::STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
self::UNSAFE_URL,
];
$this->checkValues($policy, $available);
$this->headers['referrer-policy'] = $policy;
return $this;
}
/**
* X-Frame-Options
*
* @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
* @param string $option Option value. Available Values: 'deny', 'sameorigin', 'allow-from <uri>'
* @param string|null $url URL if mode is `allow-from`
* @return $this
*/
public function setXFrameOptions(string $option = self::SAMEORIGIN, ?string $url = null)
{
$this->checkValues($option, [self::DENY, self::SAMEORIGIN, self::ALLOW_FROM]);
if ($option === self::ALLOW_FROM) {
if (empty($url)) {
throw new InvalidArgumentException('The 2nd arg $url can not be empty when `allow-from` is used');
}
$option .= ' ' . $url;
}
$this->headers['x-frame-options'] = $option;
return $this;
}
/**
* X-XSS-Protection
*
* @link https://blogs.msdn.microsoft.com/ieinternals/2011/01/31/controlling-the-xss-filter
* @param string $mode Mode value. Available Values: '1', '0', 'block'
* @return $this
*/
public function setXssProtection(string $mode = self::XSS_BLOCK)
{
if ($mode === self::XSS_BLOCK) {
$mode = self::XSS_ENABLED_BLOCK;
}
$this->checkValues($mode, [self::XSS_ENABLED, self::XSS_DISABLED, self::XSS_ENABLED_BLOCK]);
$this->headers['x-xss-protection'] = $mode;
return $this;
}
/**
* X-Permitted-Cross-Domain-Policies
*
* @link https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html
* @param string $policy Policy value. Available Values: 'all', 'none', 'master-only', 'by-content-type',
* 'by-ftp-filename'
* @return $this
*/
public function setCrossDomainPolicy(string $policy = self::ALL)
{
$this->checkValues($policy, [
self::ALL,
self::NONE,
self::MASTER_ONLY,
self::BY_CONTENT_TYPE,
self::BY_FTP_FILENAME,
]);
$this->headers['x-permitted-cross-domain-policies'] = $policy;
return $this;
}
/**
* Convenience method to check if a value is in the list of allowed args
*
* @throws \InvalidArgumentException Thrown when a value is invalid.
* @param string $value Value to check
* @param array<string> $allowed List of allowed values
* @return void
*/
protected function checkValues(string $value, array $allowed): void
{
if (!in_array($value, $allowed, true)) {
throw new InvalidArgumentException(sprintf(
'Invalid arg `%s`, use one of these: %s',
$value,
implode(', ', $allowed)
));
}
}
/**
* Serve assets if the path matches one.
*
* @param \Psr\Http\Message\ServerRequestInterface $request The request.
* @param \Psr\Http\Server\RequestHandlerInterface $handler The request handler.
* @return \Psr\Http\Message\ResponseInterface A response.
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$response = $handler->handle($request);
foreach ($this->headers as $header => $value) {
$response = $response->withHeader($header, $value);
}
return $response;
}
}