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