• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codeigniter4 / CodeIgniter4 / 23338511562

20 Mar 2026 10:18AM UTC coverage: 86.658% (+0.003%) from 86.655%
23338511562

Pull #10053

github

web-flow
Merge 0b4099cad into 5f3e55ab9
Pull Request #10053: feat: Add granular nonces

28 of 28 new or added lines in 3 files covered. (100.0%)

2 existing lines in 2 files now uncovered.

22617 of 26099 relevant lines covered (86.66%)

220.35 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.44
/system/HTTP/ContentSecurityPolicy.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\HTTP;
15

16
use CodeIgniter\Exceptions\InvalidArgumentException;
17
use Config\App;
18
use Config\ContentSecurityPolicy as ContentSecurityPolicyConfig;
19

20
/**
21
 * Provides tools for working with the Content-Security-Policy header
22
 * to help defeat XSS attacks.
23
 *
24
 * @see http://www.w3.org/TR/CSP/
25
 * @see http://www.html5rocks.com/en/tutorials/security/content-security-policy/
26
 * @see http://content-security-policy.com/
27
 * @see https://www.owasp.org/index.php/Content_Security_Policy
28
 * @see \CodeIgniter\HTTP\ContentSecurityPolicyTest
29
 */
30
class ContentSecurityPolicy
31
{
32
    private const DIRECTIVES_ALLOWING_SOURCE_LISTS = [
33
        'base-uri'        => 'baseURI',
34
        'child-src'       => 'childSrc',
35
        'connect-src'     => 'connectSrc',
36
        'default-src'     => 'defaultSrc',
37
        'font-src'        => 'fontSrc',
38
        'form-action'     => 'formAction',
39
        'frame-ancestors' => 'frameAncestors',
40
        'frame-src'       => 'frameSrc',
41
        'img-src'         => 'imageSrc',
42
        'media-src'       => 'mediaSrc',
43
        'object-src'      => 'objectSrc',
44
        'plugin-types'    => 'pluginTypes',
45
        'script-src'      => 'scriptSrc',
46
        'style-src'       => 'styleSrc',
47
        'sandbox'         => 'sandbox',
48
        'manifest-src'    => 'manifestSrc',
49
        'script-src-elem' => 'scriptSrcElem',
50
        'script-src-attr' => 'scriptSrcAttr',
51
        'style-src-elem'  => 'styleSrcElem',
52
        'style-src-attr'  => 'styleSrcAttr',
53
        'worker-src'      => 'workerSrc',
54
    ];
55

56
    /**
57
     * Map of CSP directives to this class's properties.
58
     *
59
     * @var array<string, string>
60
     */
61
    protected array $directives = [
62
        ...self::DIRECTIVES_ALLOWING_SOURCE_LISTS,
63
        'report-uri' => 'reportURI',
64
        'report-to'  => 'reportTo',
65
    ];
66

67
    /**
68
     * The `base-uri` directive restricts the URLs that can be used to specify the document base URL.
69
     *
70
     * @var array<string, bool>|string|null
71
     */
72
    protected $baseURI = [];
73

74
    /**
75
     * The `child-src` directive governs the creation of nested browsing contexts as well
76
     * as Worker execution contexts.
77
     *
78
     * @var array<string, bool>|string
79
     */
80
    protected $childSrc = [];
81

82
    /**
83
     * The `connect-src` directive restricts which URLs the protected resource can load using script interfaces.
84
     *
85
     * @var array<string, bool>|string
86
     */
87
    protected $connectSrc = [];
88

89
    /**
90
     * The `default-src` directive sets a default source list for a number of directives.
91
     *
92
     * @var array<string, bool>|string|null
93
     */
94
    protected $defaultSrc = [];
95

96
    /**
97
     * The `font-src` directive restricts from where the protected resource can load fonts.
98
     *
99
     * @var array<string, bool>|string
100
     */
101
    protected $fontSrc = [];
102

103
    /**
104
     * The `form-action` directive restricts which URLs can be used as the action of HTML form elements.
105
     *
106
     * @var array<string, bool>|string
107
     */
108
    protected $formAction = [];
109

110
    /**
111
     * The `frame-ancestors` directive indicates whether the user agent should allow embedding
112
     * the resource using a `frame`, `iframe`, `object`, `embed` or `applet` element,
113
     * or equivalent functionality in non-HTML resources.
114
     *
115
     * @var array<string, bool>|string
116
     */
117
    protected $frameAncestors = [];
118

119
    /**
120
     * The `frame-src` directive restricts the URLs which may be loaded into child navigables.
121
     *
122
     * @var array<string, bool>|string
123
     */
124
    protected $frameSrc = [];
125

126
    /**
127
     * The `img-src` directive restricts from where the protected resource can load images.
128
     *
129
     * @var array<string, bool>|string
130
     */
131
    protected $imageSrc = [];
132

133
    /**
134
     * The `media-src` directive restricts from where the protected resource can load video,
135
     * audio, and associated text tracks.
136
     *
137
     * @var array<string, bool>|string
138
     */
139
    protected $mediaSrc = [];
140

141
    /**
142
     * The `object-src` directive restricts from where the protected resource can load plugins.
143
     *
144
     * @var array<string, bool>|string
145
     */
146
    protected $objectSrc = [];
147

148
    /**
149
     * The `plugin-types` directive restricts the set of plugins that can be invoked by the
150
     * protected resource by limiting the types of resources that can be embedded.
151
     *
152
     * @var array<string, bool>|string
153
     */
154
    protected $pluginTypes = [];
155

156
    /**
157
     * The `script-src` directive restricts which scripts the protected resource can execute.
158
     *
159
     * @var array<string, bool>|string
160
     */
161
    protected $scriptSrc = [];
162

163
    /**
164
     * The `style-src` directive restricts which styles the user may applies to the protected resource.
165
     *
166
     * @var array<string, bool>|string
167
     */
168
    protected $styleSrc = [];
169

170
    /**
171
     * The `sandbox` directive specifies an HTML sandbox policy that the user agent applies to the protected resource.
172
     *
173
     * @var array<string, bool>|string
174
     */
175
    protected $sandbox = [];
176

177
    /**
178
     * The `report-uri` directive specifies a URL to which the user agent sends reports about policy violation.
179
     *
180
     * @var string|null
181
     */
182
    protected $reportURI;
183

184
    /**
185
     * The `report-to` directive specifies a named group in a Reporting API
186
     * endpoint to which the user agent sends reports about policy violation.
187
     */
188
    protected ?string $reportTo = null;
189

190
    // --------------------------------------------------------------
191
    // CSP Level 3 Directives
192
    // --------------------------------------------------------------
193

194
    /**
195
     * The `manifest-src` directive restricts the URLs from which application manifests may be loaded.
196
     *
197
     * @var array<string, bool>|string
198
     */
199
    protected $manifestSrc = [];
200

201
    /**
202
     * The `script-src-elem` directive applies to all script requests and script blocks.
203
     *
204
     * @var array<string, bool>|string
205
     */
206
    protected array|string $scriptSrcElem = [];
207

208
    /**
209
     * The `script-src-attr` directive applies to event handlers and, if present,
210
     * it will override the `script-src` directive for relevant checks.
211
     *
212
     * @var array<string, bool>|string
213
     */
214
    protected array|string $scriptSrcAttr = [];
215

216
    /**
217
     * The `style-src-elem` directive governs the behaviour of styles except
218
     * for styles defined in inline attributes.
219
     *
220
     * @var array<string, bool>|string
221
     */
222
    protected array|string $styleSrcElem = [];
223

224
    /**
225
     * The `style-src-attr` directive governs the behaviour of style attributes.
226
     *
227
     * @var array<string, bool>|string
228
     */
229
    protected array|string $styleSrcAttr = [];
230

231
    /**
232
     * The `worker-src` directive restricts the URLs which may be loaded as a `Worker`,
233
     * `SharedWorker`, or `ServiceWorker`.
234
     *
235
     * @var array<string, bool>|string
236
     */
237
    protected array|string $workerSrc = [];
238

239
    /**
240
     * Instructs user agents to rewrite URL schemes by changing HTTP to HTTPS.
241
     *
242
     * @var bool
243
     */
244
    protected $upgradeInsecureRequests = false;
245

246
    /**
247
     * Set to `true` to make all directives report-only instead of enforced.
248
     *
249
     * @var bool
250
     */
251
    protected $reportOnly = false;
252

253
    /**
254
     * Set of valid keyword-sources.
255
     *
256
     * @see https://www.w3.org/TR/CSP3/#source-expression
257
     *
258
     * @var list<string>
259
     */
260
    protected $validSources = [
261
        // CSP2 keywords
262
        'self',
263
        'none',
264
        'unsafe-inline',
265
        'unsafe-eval',
266
        // CSP3 keywords
267
        'strict-dynamic',
268
        'unsafe-hashes',
269
        'report-sample',
270
        'unsafe-allow-redirects',
271
        'wasm-unsafe-eval',
272
        'trusted-types-eval',
273
        'report-sha256',
274
        'report-sha384',
275
        'report-sha512',
276
    ];
277

278
    /**
279
     * Set of nonces generated.
280
     *
281
     * @var list<string>
282
     *
283
     * @deprecated 4.7.0 Never used.
284
     */
285
    protected $nonces = [];
286

287
    /**
288
     * Nonce for style tags.
289
     *
290
     * @var string|null
291
     */
292
    protected $styleNonce;
293

294
    /**
295
     * Nonce for script tags.
296
     *
297
     * @var string|null
298
     */
299
    protected $scriptNonce;
300

301
    /**
302
     * Whether to enable nonce to style-src and style-src-elem directives or not.
303
     *
304
     * @var bool
305
     */
306
    protected $enableStyleNonce = true;
307

308
    /**
309
     * Whether to enable nonce to script-src and script-src-elem directives or not.
310
     *
311
     * @var bool
312
     */
313
    protected $enableScriptNonce = true;
314

315
    /**
316
     * Nonce placeholder for style tags.
317
     *
318
     * @var string
319
     */
320
    protected $styleNonceTag = '{csp-style-nonce}';
321

322
    /**
323
     * Nonce placeholder for script tags.
324
     *
325
     * @var string
326
     */
327
    protected $scriptNonceTag = '{csp-script-nonce}';
328

329
    /**
330
     * Replace nonce tags automatically?
331
     *
332
     * @var bool
333
     */
334
    protected $autoNonce = true;
335

336
    /**
337
     * An array of header info since we have to build
338
     * ourselves before passing to a Response object.
339
     *
340
     * @var array<string, string>
341
     */
342
    protected $tempHeaders = [];
343

344
    /**
345
     * An array of header info to build that should only be reported.
346
     *
347
     * @var array<string, string>
348
     */
349
    protected $reportOnlyHeaders = [];
350

351
    /**
352
     * Whether Content Security Policy is being enforced.
353
     *
354
     * @var bool
355
     */
356
    protected $CSPEnabled = false;
357

358
    /**
359
     * Map of reporting endpoints to their URLs.
360
     *
361
     * @var array<string, string>
362
     */
363
    private array $reportingEndpoints = [];
364

365
    /**
366
     * Stores our default values from the Config file.
367
     */
368
    public function __construct(ContentSecurityPolicyConfig $config)
369
    {
370
        $this->CSPEnabled = config(App::class)->CSPEnabled;
641✔
371

372
        foreach (get_object_vars($config) as $setting => $value) {
641✔
373
            if (! property_exists($this, $setting)) {
641✔
UNCOV
374
                continue;
×
375
            }
376

377
            if (
378
                in_array($setting, self::DIRECTIVES_ALLOWING_SOURCE_LISTS, true)
641✔
379
                && is_array($value)
641✔
380
                && array_is_list($value)
641✔
381
            ) {
382
                // Config sets these directives as `list<string>|string`
383
                // but we need them as `array<string, bool>` internally.
384
                $this->{$setting} = array_combine($value, array_fill(0, count($value), $this->reportOnly));
641✔
385

386
                continue;
641✔
387
            }
388

389
            $this->{$setting} = $value;
641✔
390
        }
391

392
        if (! is_array($this->styleSrc)) {
641✔
393
            $this->styleSrc = [$this->styleSrc => $this->reportOnly];
641✔
394
        }
395

396
        if (! is_array($this->scriptSrc)) {
641✔
397
            $this->scriptSrc = [$this->scriptSrc => $this->reportOnly];
641✔
398
        }
399
    }
400

401
    /**
402
     * Whether Content Security Policy is being enforced.
403
     */
404
    public function enabled(): bool
405
    {
406
        return $this->CSPEnabled;
161✔
407
    }
408

409
    /**
410
     * Get the nonce for the style tag.
411
     */
412
    public function getStyleNonce(): string
413
    {
414
        if ($this->styleNonce === null) {
11✔
415
            $this->styleNonce = base64_encode(random_bytes(12));
11✔
416

417
            if ($this->enableStyleNonce) {
11✔
418
                $this->addStyleSrc('nonce-' . $this->styleNonce);
11✔
419

420
                if ($this->styleSrcElem !== []) {
11✔
421
                    $this->addStyleSrcElem('nonce-' . $this->styleNonce);
10✔
422
                }
423
            }
424
        }
425

426
        return $this->styleNonce;
11✔
427
    }
428

429
    /**
430
     * Get the nonce for the script tag.
431
     */
432
    public function getScriptNonce(): string
433
    {
434
        if ($this->scriptNonce === null) {
14✔
435
            $this->scriptNonce = base64_encode(random_bytes(12));
14✔
436

437
            if ($this->enableScriptNonce) {
14✔
438
                $this->addScriptSrc('nonce-' . $this->scriptNonce);
14✔
439

440
                if ($this->scriptSrcElem !== []) {
14✔
441
                    $this->addScriptSrcElem('nonce-' . $this->scriptNonce);
13✔
442
                }
443
            }
444
        }
445

446
        return $this->scriptNonce;
14✔
447
    }
448

449
    /**
450
     * Compiles and sets the appropriate headers in the request.
451
     *
452
     * Should be called just prior to sending the response to the user agent.
453
     *
454
     * @return void
455
     */
456
    public function finalize(ResponseInterface $response)
457
    {
458
        $this->generateNonces($response);
127✔
459

460
        $this->buildHeaders($response);
127✔
461
    }
462

463
    /**
464
     * If TRUE, nothing will be restricted. Instead all violations will
465
     * be reported to the reportURI for monitoring. This is useful when
466
     * you are just starting to implement the policy, and will help
467
     * determine what errors need to be addressed before you turn on
468
     * all filtering.
469
     *
470
     * @return $this
471
     */
472
    public function reportOnly(bool $value = true)
473
    {
474
        $this->reportOnly = $value;
10✔
475

476
        return $this;
10✔
477
    }
478

479
    /**
480
     * Adds a new value to the `base-uri` directive.
481
     *
482
     * `base-uri` restricts the URLs that can appear in a page's <base> element.
483
     *
484
     * @see http://www.w3.org/TR/CSP/#directive-base-uri
485
     *
486
     * @param list<string>|string $uri
487
     *
488
     * @return $this
489
     */
490
    public function addBaseURI($uri, ?bool $explicitReporting = null)
491
    {
492
        $this->addOption($uri, 'baseURI', $explicitReporting ?? $this->reportOnly);
3✔
493

494
        return $this;
3✔
495
    }
496

497
    /**
498
     * Adds a new value to the `child-src` directive.
499
     *
500
     * `child-src` lists the URLs for workers and embedded frame contents.
501
     * For example: child-src https://youtube.com would enable embedding
502
     * videos from YouTube but not from other origins.
503
     *
504
     * @see http://www.w3.org/TR/CSP/#directive-child-src
505
     *
506
     * @param list<string>|string $uri
507
     *
508
     * @return $this
509
     */
510
    public function addChildSrc($uri, ?bool $explicitReporting = null)
511
    {
512
        $this->addOption($uri, 'childSrc', $explicitReporting ?? $this->reportOnly);
2✔
513

514
        return $this;
2✔
515
    }
516

517
    /**
518
     * Adds a new value to the `connect-src` directive.
519
     *
520
     * `connect-src` limits the origins to which you can connect
521
     * (via XHR, WebSockets, and EventSource).
522
     *
523
     * @see http://www.w3.org/TR/CSP/#directive-connect-src
524
     *
525
     * @param list<string>|string $uri
526
     *
527
     * @return $this
528
     */
529
    public function addConnectSrc($uri, ?bool $explicitReporting = null)
530
    {
531
        $this->addOption($uri, 'connectSrc', $explicitReporting ?? $this->reportOnly);
1✔
532

533
        return $this;
1✔
534
    }
535

536
    /**
537
     * Adds a new value to the `default-src` directive.
538
     *
539
     * `default-src` is the URI that is used for many of the settings when
540
     * no other source has been set.
541
     *
542
     * @see http://www.w3.org/TR/CSP/#directive-default-src
543
     *
544
     * @param list<string>|string $uri
545
     *
546
     * @return $this
547
     */
548
    public function setDefaultSrc($uri, ?bool $explicitReporting = null)
549
    {
550
        $this->defaultSrc = [(string) $uri => $explicitReporting ?? $this->reportOnly];
1✔
551

552
        return $this;
1✔
553
    }
554

555
    /**
556
     * Adds a new value to the `font-src` directive.
557
     *
558
     * `font-src` specifies the origins that can serve web fonts.
559
     *
560
     * @see http://www.w3.org/TR/CSP/#directive-font-src
561
     *
562
     * @param list<string>|string $uri
563
     *
564
     * @return $this
565
     */
566
    public function addFontSrc($uri, ?bool $explicitReporting = null)
567
    {
568
        $this->addOption($uri, 'fontSrc', $explicitReporting ?? $this->reportOnly);
3✔
569

570
        return $this;
3✔
571
    }
572

573
    /**
574
     * Adds a new value to the `form-action` directive.
575
     *
576
     * @see http://www.w3.org/TR/CSP/#directive-form-action
577
     *
578
     * @param list<string>|string $uri
579
     *
580
     * @return $this
581
     */
582
    public function addFormAction($uri, ?bool $explicitReporting = null)
583
    {
584
        $this->addOption($uri, 'formAction', $explicitReporting ?? $this->reportOnly);
2✔
585

586
        return $this;
2✔
587
    }
588

589
    /**
590
     * Adds a new value to the `frame-ancestors` directive.
591
     *
592
     * @see http://www.w3.org/TR/CSP/#directive-frame-ancestors
593
     *
594
     * @param list<string>|string $uri
595
     *
596
     * @return $this
597
     */
598
    public function addFrameAncestor($uri, ?bool $explicitReporting = null)
599
    {
600
        $this->addOption($uri, 'frameAncestors', $explicitReporting ?? $this->reportOnly);
2✔
601

602
        return $this;
2✔
603
    }
604

605
    /**
606
     * Adds a new value to the `frame-src` directive.
607
     *
608
     * @see http://www.w3.org/TR/CSP/#directive-frame-src
609
     *
610
     * @param list<string>|string $uri
611
     *
612
     * @return $this
613
     */
614
    public function addFrameSrc($uri, ?bool $explicitReporting = null)
615
    {
616
        $this->addOption($uri, 'frameSrc', $explicitReporting ?? $this->reportOnly);
2✔
617

618
        return $this;
2✔
619
    }
620

621
    /**
622
     * Adds a new value to the `img-src` directive.
623
     *
624
     * @see http://www.w3.org/TR/CSP/#directive-img-src
625
     *
626
     * @param list<string>|string $uri
627
     *
628
     * @return $this
629
     */
630
    public function addImageSrc($uri, ?bool $explicitReporting = null)
631
    {
632
        $this->addOption($uri, 'imageSrc', $explicitReporting ?? $this->reportOnly);
5✔
633

634
        return $this;
5✔
635
    }
636

637
    /**
638
     * Adds a new value to the `media-src` directive.
639
     *
640
     * @see http://www.w3.org/TR/CSP/#directive-media-src
641
     *
642
     * @param list<string>|string $uri
643
     *
644
     * @return $this
645
     */
646
    public function addMediaSrc($uri, ?bool $explicitReporting = null)
647
    {
648
        $this->addOption($uri, 'mediaSrc', $explicitReporting ?? $this->reportOnly);
2✔
649

650
        return $this;
2✔
651
    }
652

653
    /**
654
     * Adds a new value to the `manifest-src` directive.
655
     *
656
     * @see https://www.w3.org/TR/CSP/#directive-manifest-src
657
     *
658
     * @param list<string>|string $uri
659
     *
660
     * @return $this
661
     */
662
    public function addManifestSrc($uri, ?bool $explicitReporting = null)
663
    {
664
        $this->addOption($uri, 'manifestSrc', $explicitReporting ?? $this->reportOnly);
2✔
665

666
        return $this;
2✔
667
    }
668

669
    /**
670
     * Adds a new value to the `object-src` directive.
671
     *
672
     * @see http://www.w3.org/TR/CSP/#directive-object-src
673
     *
674
     * @param list<string>|string $uri
675
     *
676
     * @return $this
677
     */
678
    public function addObjectSrc($uri, ?bool $explicitReporting = null)
679
    {
680
        $this->addOption($uri, 'objectSrc', $explicitReporting ?? $this->reportOnly);
2✔
681

682
        return $this;
2✔
683
    }
684

685
    /**
686
     * Adds a new value to the `plugin-types` directive.
687
     *
688
     * @see http://www.w3.org/TR/CSP/#directive-plugin-types
689
     *
690
     * @param list<string>|string $mime
691
     *
692
     * @return $this
693
     */
694
    public function addPluginType($mime, ?bool $explicitReporting = null)
695
    {
696
        $this->addOption($mime, 'pluginTypes', $explicitReporting ?? $this->reportOnly);
2✔
697

698
        return $this;
2✔
699
    }
700

701
    /**
702
     * Adds a new value to the `sandbox` directive.
703
     *
704
     * @see http://www.w3.org/TR/CSP/#directive-sandbox
705
     *
706
     * @param list<string>|string $flags
707
     *
708
     * @return $this
709
     */
710
    public function addSandbox($flags, ?bool $explicitReporting = null)
711
    {
712
        $this->addOption($flags, 'sandbox', $explicitReporting ?? $this->reportOnly);
2✔
713

714
        return $this;
2✔
715
    }
716

717
    /**
718
     * Adds a new value to the `script-src` directive.
719
     *
720
     * @see http://www.w3.org/TR/CSP/#directive-script-src
721
     *
722
     * @param list<string>|string $uri
723
     *
724
     * @return $this
725
     */
726
    public function addScriptSrc($uri, ?bool $explicitReporting = null)
727
    {
728
        $this->addOption($uri, 'scriptSrc', $explicitReporting ?? $this->reportOnly);
18✔
729

730
        return $this;
18✔
731
    }
732

733
    /**
734
     * Adds a new value to the `script-src-elem` directive.
735
     *
736
     * @see https://www.w3.org/TR/CSP/#directive-script-src-elem
737
     *
738
     * @param list<string>|string $uri
739
     */
740
    public function addScriptSrcElem(array|string $uri, ?bool $explicitReporting = null): static
741
    {
742
        $this->addOption($uri, 'scriptSrcElem', $explicitReporting ?? $this->reportOnly);
14✔
743

744
        return $this;
14✔
745
    }
746

747
    /**
748
     * Adds a new value to the `script-src-attr` directive.
749
     *
750
     * @see https://www.w3.org/TR/CSP/#directive-script-src-attr
751
     *
752
     * @param list<string>|string $uri
753
     */
754
    public function addScriptSrcAttr(array|string $uri, ?bool $explicitReporting = null): static
755
    {
756
        $this->addOption($uri, 'scriptSrcAttr', $explicitReporting ?? $this->reportOnly);
1✔
757

758
        return $this;
1✔
759
    }
760

761
    /**
762
     * Adds a new value to the `style-src` directive.
763
     *
764
     * @see http://www.w3.org/TR/CSP/#directive-style-src
765
     *
766
     * @param list<string>|string $uri
767
     *
768
     * @return $this
769
     */
770
    public function addStyleSrc($uri, ?bool $explicitReporting = null)
771
    {
772
        $this->addOption($uri, 'styleSrc', $explicitReporting ?? $this->reportOnly);
16✔
773

774
        return $this;
16✔
775
    }
776

777
    /**
778
     * Adds a new value to the `style-src-elem` directive.
779
     *
780
     * @see https://www.w3.org/TR/CSP/#directive-style-src-elem
781
     *
782
     * @param list<string>|string $uri
783
     */
784
    public function addStyleSrcElem(array|string $uri, ?bool $explicitReporting = null): static
785
    {
786
        $this->addOption($uri, 'styleSrcElem', $explicitReporting ?? $this->reportOnly);
11✔
787

788
        return $this;
11✔
789
    }
790

791
    /**
792
     * Adds a new value to the `style-src-attr` directive.
793
     *
794
     * @see https://www.w3.org/TR/CSP/#directive-style-src-attr
795
     *
796
     * @param list<string>|string $uri
797
     */
798
    public function addStyleSrcAttr(array|string $uri, ?bool $explicitReporting = null): static
799
    {
800
        $this->addOption($uri, 'styleSrcAttr', $explicitReporting ?? $this->reportOnly);
1✔
801

802
        return $this;
1✔
803
    }
804

805
    /**
806
     * Adds a new value to the `worker-src` directive.
807
     *
808
     * @see https://www.w3.org/TR/CSP/#directive-worker-src
809
     *
810
     * @param list<string>|string $uri
811
     */
812
    public function addWorkerSrc($uri, ?bool $explicitReporting = null): static
813
    {
814
        $this->addOption($uri, 'workerSrc', $explicitReporting ?? $this->reportOnly);
1✔
815

816
        return $this;
1✔
817
    }
818

819
    /**
820
     * Sets whether the user agents should rewrite URL schemes, changing HTTP to HTTPS.
821
     *
822
     * @return $this
823
     */
824
    public function upgradeInsecureRequests(bool $value = true)
825
    {
826
        $this->upgradeInsecureRequests = $value;
1✔
827

828
        return $this;
1✔
829
    }
830

831
    /**
832
     * Specifies a URL where a browser will send reports when a content
833
     * security policy is violated.
834
     *
835
     * @see http://www.w3.org/TR/CSP/#directive-report-uri
836
     *
837
     * @param string $uri URL to send reports. Set `''` if you want to remove
838
     *                    this directive at runtime.
839
     *
840
     * @return $this
841
     */
842
    public function setReportURI(string $uri)
843
    {
844
        $this->reportURI = $uri;
3✔
845

846
        return $this;
3✔
847
    }
848

849
    /**
850
     * Specifies a named group in a Reporting API endpoint to which the user
851
     * agent sends reports about policy violation.
852
     *
853
     * @see https://www.w3.org/TR/CSP/#directive-report-to
854
     *
855
     * @param string $endpoint The name of the reporting endpoint. Set `''` if you
856
     *                         want to remove this directive at runtime.
857
     */
858
    public function setReportToEndpoint(string $endpoint): static
859
    {
860
        if ($endpoint === '') {
5✔
861
            $this->reportURI = null;
1✔
862
            $this->reportTo  = null;
1✔
863

864
            return $this;
1✔
865
        }
866

867
        if (! array_key_exists($endpoint, $this->reportingEndpoints)) {
5✔
868
            throw new InvalidArgumentException(sprintf('The reporting endpoint "%s" has not been defined.', $endpoint));
1✔
869
        }
870

871
        $this->reportURI = $this->reportingEndpoints[$endpoint]; // for BC with browsers that do not support `report-to`
4✔
872
        $this->reportTo  = $endpoint;
4✔
873

874
        return $this;
4✔
875
    }
876

877
    /**
878
     * Adds reporting endpoints to the `Reporting-Endpoints` header.
879
     *
880
     * @param array<string, string> $endpoint
881
     */
882
    public function addReportingEndpoints(array $endpoint): static
883
    {
884
        foreach ($endpoint as $name => $url) {
4✔
885
            $this->reportingEndpoints[$name] = $url;
4✔
886
        }
887

888
        return $this;
4✔
889
    }
890

891
    /**
892
     * Enables or disables adding nonces to style-src and style-src-elem directives.
893
     *
894
     * @return $this
895
     */
896
    public function setEnableStyleNonce(bool $value = true): static
897
    {
898
        $this->enableStyleNonce = $value;
1✔
899

900
        return $this;
1✔
901
    }
902

903
    /**
904
     * Enables or disables adding nonces to script-src and script-src-elem directives.
905
     *
906
     * @return $this
907
     */
908
    public function setEnableScriptNonce(bool $value = true): static
909
    {
910
        $this->enableScriptNonce = $value;
1✔
911

912
        return $this;
1✔
913
    }
914

915
    /**
916
     * DRY method to add an string or array to a class property.
917
     *
918
     * @param list<string>|string $options
919
     *
920
     * @return void
921
     */
922
    protected function addOption($options, string $target, ?bool $explicitReporting = null)
923
    {
924
        // Ensure we have an array to work with...
925
        if (is_string($this->{$target})) {
54✔
926
            $this->{$target} = [$this->{$target} => $this->reportOnly];
30✔
927
        }
928

929
        $options = is_array($options) ? $options : [$options];
54✔
930

931
        foreach ($options as $option) {
54✔
932
            $this->{$target}[$option] = $explicitReporting ?? $this->reportOnly;
54✔
933
        }
934
    }
935

936
    /**
937
     * Scans the body of the request message and replaces any nonce
938
     * placeholders with actual nonces, that we'll then add to our
939
     * headers.
940
     *
941
     * @return void
942
     */
943
    protected function generateNonces(ResponseInterface $response)
944
    {
945
        if ($this->enabled() && ! $this->autoNonce) {
127✔
946
            return;
2✔
947
        }
948

949
        $body = (string) $response->getBody();
125✔
950

951
        if ($body === '') {
125✔
952
            return;
15✔
953
        }
954

955
        // Escape quotes for JSON responses to prevent corrupting the JSON body
956
        $jsonEscape = str_contains($response->getHeaderLine('Content-Type'), 'json');
110✔
957

958
        // Replace style and script placeholders with nonces
959
        $pattern = sprintf('/(%s|%s)/', preg_quote($this->styleNonceTag, '/'), preg_quote($this->scriptNonceTag, '/'));
110✔
960

961
        $body = preg_replace_callback($pattern, function ($match) use ($jsonEscape): string {
110✔
962
            if (! $this->enabled()) {
20✔
963
                return '';
14✔
964
            }
965

966
            $nonce = $match[0] === $this->styleNonceTag ? $this->getStyleNonce() : $this->getScriptNonce();
6✔
967
            $attr  = 'nonce="' . $nonce . '"';
6✔
968

969
            return $jsonEscape ? str_replace('"', '\\"', $attr) : $attr;
6✔
970
        }, $body);
110✔
971

972
        $response->setBody($body);
110✔
973
    }
974

975
    /**
976
     * Based on the current state of the elements, will add the appropriate
977
     * Content-Security-Policy and Content-Security-Policy-Report-Only headers
978
     * with their values to the response object.
979
     *
980
     * @return void
981
     */
982
    protected function buildHeaders(ResponseInterface $response)
983
    {
984
        if (! $this->enabled()) {
127✔
985
            return;
70✔
986
        }
987

988
        $response->setHeader('Content-Security-Policy', []);
57✔
989
        $response->setHeader('Content-Security-Policy-Report-Only', []);
57✔
990
        $response->setHeader('Reporting-Endpoints', []);
57✔
991

992
        if (in_array($this->baseURI, ['', null, []], true)) {
57✔
993
            $this->baseURI = 'self';
54✔
994
        }
995

996
        if (in_array($this->defaultSrc, ['', null, []], true)) {
57✔
997
            $this->defaultSrc = 'self';
55✔
998
        }
999

1000
        foreach ($this->directives as $name => $property) {
57✔
1001
            if ($name === 'report-uri' && (string) $this->reportURI === '') {
57✔
1002
                continue;
54✔
1003
            }
1004

1005
            if ($name === 'report-to' && (string) $this->reportTo === '') {
57✔
1006
                continue;
55✔
1007
            }
1008

1009
            if ($this->{$property} !== null) {
57✔
1010
                $this->addToHeader($name, $this->{$property});
57✔
1011
            }
1012
        }
1013

1014
        // Compile our own header strings here since if we just
1015
        // append it to the response, it will be joined with
1016
        // commas, not semi-colons as we need.
1017
        if ($this->reportingEndpoints !== []) {
57✔
1018
            $endpoints = [];
4✔
1019

1020
            foreach ($this->reportingEndpoints as $name => $url) {
4✔
1021
                $endpoints[] = trim("{$name}=\"{$url}\"");
4✔
1022
            }
1023

1024
            $response->appendHeader('Reporting-Endpoints', implode(', ', $endpoints));
4✔
1025
            $this->reportingEndpoints = [];
4✔
1026
        }
1027

1028
        if ($this->tempHeaders !== []) {
57✔
1029
            $header = [];
57✔
1030

1031
            foreach ($this->tempHeaders as $name => $value) {
57✔
1032
                $header[] = trim("{$name} {$value}");
57✔
1033
            }
1034

1035
            if ($this->upgradeInsecureRequests) {
57✔
1036
                $header[] = 'upgrade-insecure-requests';
1✔
1037
            }
1038

1039
            $response->appendHeader('Content-Security-Policy', implode('; ', $header));
57✔
1040
            $this->tempHeaders = [];
57✔
1041
        }
1042

1043
        if ($this->reportOnlyHeaders !== []) {
57✔
1044
            $header = [];
17✔
1045

1046
            foreach ($this->reportOnlyHeaders as $name => $value) {
17✔
1047
                $header[] = trim("{$name} {$value}");
17✔
1048
            }
1049

1050
            $response->appendHeader('Content-Security-Policy-Report-Only', implode('; ', $header));
17✔
1051
            $this->reportOnlyHeaders = [];
17✔
1052
        }
1053
    }
1054

1055
    /**
1056
     * Adds a directive and its options to the appropriate header. The $values
1057
     * array might have options that are geared toward either the regular or the
1058
     * reportOnly header, since it's viable to have both simultaneously.
1059
     *
1060
     * @param array<string, bool>|string $values
1061
     *
1062
     * @return void
1063
     */
1064
    protected function addToHeader(string $name, $values = null)
1065
    {
1066
        if (is_string($values)) {
57✔
1067
            $values = [$values => $this->reportOnly];
57✔
1068
        }
1069

1070
        $sources       = [];
57✔
1071
        $reportSources = [];
57✔
1072

1073
        foreach ($values as $value => $reportOnly) {
57✔
1074
            if (
1075
                in_array($value, $this->validSources, true)
57✔
1076
                || str_starts_with($value, 'nonce-')
43✔
1077
                || str_starts_with($value, 'sha256-')
36✔
1078
                || str_starts_with($value, 'sha384-')
36✔
1079
                || str_starts_with($value, 'sha512-')
57✔
1080
            ) {
1081
                $value = "'{$value}'";
57✔
1082
            }
1083

1084
            if ($reportOnly) {
57✔
1085
                $reportSources[] = $value;
17✔
1086
            } else {
1087
                $sources[] = $value;
57✔
1088
            }
1089
        }
1090

1091
        if ($sources !== []) {
57✔
1092
            $this->tempHeaders[$name] = implode(' ', $sources);
57✔
1093
        }
1094

1095
        if ($reportSources !== []) {
57✔
1096
            $this->reportOnlyHeaders[$name] = implode(' ', $reportSources);
17✔
1097
        }
1098
    }
1099

1100
    public function clearDirective(string $directive): void
1101
    {
1102
        if (! array_key_exists($directive, $this->directives)) {
8✔
1103
            return;
1✔
1104
        }
1105

1106
        if ($directive === 'report-uri') {
8✔
1107
            $this->reportURI = null;
1✔
1108

1109
            return;
1✔
1110
        }
1111

1112
        if ($directive === 'report-to') {
8✔
1113
            $this->reportURI = null;
1✔
1114
            $this->reportTo  = null;
1✔
1115

1116
            return;
1✔
1117
        }
1118

1119
        $this->{$this->directives[$directive]} = [];
8✔
1120
    }
1121
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc