ContentSecurityPolicy::hashes()   A
last analyzed

Complexity

Conditions 5
Paths 3

Size

Total Lines 21
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 5
eloc 10
c 3
b 0
f 0
nc 3
nop 1
dl 0
loc 21
rs 9.6111
1
<?php
2
3
/**
4
 * Platine Framework
5
 *
6
 * Platine Framework is a lightweight, high-performance, simple and elegant PHP
7
 * Web framework
8
 *
9
 * This content is released under the MIT License (MIT)
10
 *
11
 * Copyright (c) 2020 Platine Framework
12
 * Copyright (c) 2015 - 2023 Paragon Initiative Enterprises
13
 *
14
 * Permission is hereby granted, free of charge, to any person obtaining a copy
15
 * of this software and associated documentation files (the "Software"), to deal
16
 * in the Software without restriction, including without limitation the rights
17
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
 * copies of the Software, and to permit persons to whom the Software is
19
 * furnished to do so, subject to the following conditions:
20
 *
21
 * The above copyright notice and this permission notice shall be included in all
22
 * copies or substantial portions of the Software.
23
 *
24
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
 * SOFTWARE.
31
 */
32
33
/**
34
 *  @file ContentSecurityPolicy.php
35
 *
36
 *  The Content Security Policy class
37
 *
38
 *  @package    Platine\Framework\Security\Policy
39
 *  @author Platine Developers team
40
 *  @copyright  Copyright (c) 2020
41
 *  @license    http://opensource.org/licenses/MIT  MIT License
42
 *  @link   https://www.platine-php.com
43
 *  @version 1.0.0
44
 *  @filesource
45
 */
46
47
declare(strict_types=1);
48
49
namespace Platine\Framework\Security\Policy;
50
51
/**
52
 * @class ContentSecurityPolicy
53
 * @package Platine\Framework\Security\Policy
54
 */
55
class ContentSecurityPolicy extends AbstractPolicy
56
{
57
    /**
58
     * Content Security Policy white list directives.
59
     * @var array<string, bool>
60
     */
61
    protected array $whitelist = [
62
        'base-uri' => true,
63
        'child-src' => true,
64
        'connect-src' => true,
65
        'default-src' => true,
66
        'font-src' => true,
67
        'form-action' => true,
68
        'frame-ancestors' => true,
69
        'frame-src' => true,
70
        'img-src' => true,
71
        'manifest-src' => true,
72
        'media-src' => true,
73
        'navigate-to' => true,
74
        'object-src' => true,
75
        'prefetch-src' => true,
76
        'script-src' => true,
77
        'script-src-attr' => true,
78
        'script-src-elem' => true,
79
        'style-src' => true,
80
        'style-src-attr' => true,
81
        'style-src-elem' => true,
82
        'worker-src' => true,
83
    ];
84
85
    /**
86
     * {@inheritdoc}
87
     */
88
    public function headers(): string
89
    {
90
        $headers = [
91
            $this->directives(),
92
            $this->pluginTypes(),
93
            $this->sandbox(),
94
            $this->requireTrustedTypesFor(),
95
            $this->trustedTypes(),
96
            $this->blockAllMixedContent(),
97
            $this->upgradeInsecureRequests(),
98
            $this->reportTo(),
99
            $this->reportUri(),
100
        ];
101
102
        return $this->implode(array_filter($headers), '; ');
103
    }
104
105
    /**
106
     * Build directive
107
     * @param array<string, mixed> $config
108
     * @return string
109
     */
110
    public function directive(array $config): string
111
    {
112
        if ($config['none'] ?? false) {
113
            return '\'none\'';
114
        }
115
116
        $sources = array_merge(
117
            $this->keywords($config),
118
            $this->schemes($config['schemes'] ?? []),
119
            $this->hashes($config['hashes'] ?? []),
120
            $this->nonces($config['nonces'] ?? []),
121
            $config['allow'] ?? []
122
        );
123
124
        $filtered = array_filter($sources);
125
126
        return $this->implode($filtered);
127
    }
128
129
    /**
130
     * Build directive keywords.
131
     * @param array<string, mixed> $config
132
     * @return array<string>
133
     */
134
    public function keywords(array $config): array
135
    {
136
        $whitelist = [
137
            'self' => true,
138
            'unsafe-inline' => true,
139
            'unsafe-eval' => true,
140
            'unsafe-hashes' => true,
141
            'strict-dynamic' => true,
142
            'report-sample' => true,
143
            'unsafe-allow-redirects' => true,
144
        ];
145
146
        $filtered = $this->filter($config, $whitelist);
147
148
        return array_map(function (string $keyword) {
149
            return sprintf('\'%s\'', $keyword);
150
        }, $filtered);
151
    }
152
153
    /**
154
     * Build directive schemes
155
     * @param array<string, mixed> $schemes
156
     * @return array<string>
157
     */
158
    public function schemes(array $schemes): array
159
    {
160
        return array_map(function (string $scheme) {
161
            $clean = trim($scheme);
162
163
            if (substr($clean, -1) === ':') {
164
                return $clean;
165
            }
166
167
            return sprintf('%s:', $clean);
168
        }, $schemes);
169
    }
170
171
    /**
172
     * Build directive nonce's.
173
     * @param array<string, mixed> $nonces
174
     * @return array<string>
175
     */
176
    public function nonces(array $nonces): array
177
    {
178
        return array_map(function (string $nonce) {
179
            $clean = trim($nonce);
180
181
            if (base64_decode($clean, true) === false) {
182
                return '';
183
            }
184
185
            return sprintf('\'nonce-%s\'', $clean);
186
        }, $nonces);
187
    }
188
189
    /**
190
     * Build directive hashes.
191
     * @param array<string, mixed> $groups
192
     * @return array<string>
193
     */
194
    public function hashes(array $groups): array
195
    {
196
        $result = [];
197
198
        foreach ($groups as $hash => $items) {
199
            if (in_array($hash, ['sha256', 'sha384', 'sha512'], true) === false) {
200
                continue;
201
            }
202
203
            foreach ($items as $item) {
204
                $clean = trim($item);
205
206
                if (base64_decode($clean, true) === false) {
207
                    continue;
208
                }
209
210
                $result[] = sprintf('\'%s-%s\'', $hash, $clean);
211
            }
212
        }
213
214
        return $result;
215
    }
216
217
    /**
218
     * Build plugin-types directive.
219
     * @return string
220
     */
221
    public function pluginTypes(): string
222
    {
223
        $pluginTypes = $this->configurations['plugin-types'] ?? [];
224
225
        $filtered = array_filter($pluginTypes, function (mixed $mime): bool {
226
            return (bool) preg_match('/^[a-z\-]+\/[a-z\-]+$/i', $mime);
227
        });
228
229
        if (count($filtered) > 0) {
230
            array_unshift($filtered, 'plugin-types');
231
        }
232
233
        return $this->implode($filtered);
234
    }
235
236
    /**
237
     * Build sandbox directive.
238
     * @return string
239
     */
240
    public function sandbox(): string
241
    {
242
        $sandbox = $this->configurations['sandbox'] ?? [];
243
244
        if (($sandbox['enable'] ?? false) === false) {
245
            return '';
246
        }
247
248
        $whitelist = [
249
            'allow-downloads-without-user-activation' => true,
250
            'allow-forms' => true,
251
            'allow-modals' => true,
252
            'allow-orientation-lock' => true,
253
            'allow-pointer-lock' => true,
254
            'allow-popups' => true,
255
            'allow-popups-to-escape-sandbox' => true,
256
            'allow-presentation' => true,
257
            'allow-same-origin' => true,
258
            'allow-scripts' => true,
259
            'allow-storage-access-by-user-activation' => true,
260
            'allow-top-navigation' => true,
261
            'allow-top-navigation-by-user-activation' => true,
262
        ];
263
264
        $filtered = $this->filter($sandbox, $whitelist);
265
266
        array_unshift($filtered, 'sandbox');
267
268
        return $this->implode($filtered);
269
    }
270
271
    /**
272
     * Build require-trusted-types-for directive.
273
     * @return string
274
     */
275
    public function requireTrustedTypesFor(): string
276
    {
277
        $config = $this->configurations['require-trusted-types-for'] ?? [];
278
279
        if (($config['script'] ?? false) === false) {
280
            return '';
281
        }
282
283
        return "require-trusted-types-for 'script'";
284
    }
285
286
    /**
287
     * Build trusted-types directive.
288
     * @return string
289
     */
290
    public function trustedTypes(): string
291
    {
292
        $trustedTypes = $this->configurations['trusted-types'] ?? [];
293
294
        if (($trustedTypes['enable'] ?? false) === false) {
295
            return '';
296
        }
297
298
        $policies = array_map('trim', $trustedTypes['policies'] ?? []);
299
300
        if ($trustedTypes['default'] ?? false) {
301
            $policies[] = 'default';
302
        }
303
304
        if ($trustedTypes['allow-duplicates'] ?? false) {
305
            $policies[] = '\'allow-duplicates\'';
306
        }
307
308
        array_unshift($policies, 'trusted-types');
309
310
        return $this->implode($policies);
311
    }
312
313
    /**
314
     * Build block-all-mixed-content directive.
315
     * @return string
316
     */
317
    public function blockAllMixedContent(): string
318
    {
319
        if (($this->configurations['block-all-mixed-content'] ?? false) === false) {
320
            return '';
321
        }
322
323
        return 'block-all-mixed-content';
324
    }
325
326
    /**
327
     * Build upgrade-insecure-requests directive.
328
     * @return string
329
     */
330
    public function upgradeInsecureRequests(): string
331
    {
332
        if (($this->configurations['upgrade-insecure-requests'] ?? false) === false) {
333
            return '';
334
        }
335
336
        return 'upgrade-insecure-requests';
337
    }
338
339
    /**
340
     * Build report-to directive.
341
     * @return string
342
     */
343
    public function reportTo(): string
344
    {
345
        if (empty($this->configurations['report-to'])) {
346
            return '';
347
        }
348
349
        return sprintf('report-to %s', $this->configurations['report-to']);
350
    }
351
352
    /**
353
     * Build report-uri directive.
354
     * @return string
355
     */
356
    public function reportUri(): string
357
    {
358
        if (empty($this->configurations['report-uri'])) {
359
            return '';
360
        }
361
362
        $uri = $this->implode($this->configurations['report-uri']);
363
364
        return sprintf('report-uri %s', $uri);
365
    }
366
367
    /**
368
     * Using key to filter configuration and return keys.
369
     * @param array<string, mixed> $config
370
     * @param array<string, mixed> $available
371
     * @return array<string>
372
     */
373
    public function filter(array $config, array $available): array
374
    {
375
        $targets = array_intersect_key($config, $available);
376
377
        $needs = array_filter($targets);
378
379
        return array_keys($needs);
380
    }
381
382
    /**
383
     * Implode strings using glue
384
     * @param array<mixed> $payload
385
     * @param string $glue
386
     * @return string
387
     */
388
    public function implode(array $payload, string $glue = ' '): string
389
    {
390
        return implode($glue, $payload);
391
    }
392
393
    /**
394
     * Build the directives
395
     * @return string
396
     */
397
    protected function directives(): string
398
    {
399
        $result = [];
400
        foreach ($this->configurations as $name => $config) {
401
            if (($this->whitelist[$name] ?? false) === false) {
402
                continue;
403
            }
404
405
            $value = $this->directive($config);
406
407
            if (empty($value)) {
408
                continue;
409
            }
410
411
            $result[] = sprintf('%s %s', $name, $value);
412
        }
413
414
        return $this->implode($result, '; ');
415
    }
416
}
417