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

codeigniter4 / CodeIgniter4 / 23843775052

01 Apr 2026 10:18AM UTC coverage: 86.55% (-0.006%) from 86.556%
23843775052

Pull #10053

github

web-flow
Merge d4275af09 into f474a8878
Pull Request #10053: feat: Add granular nonces

18 of 22 new or added lines in 2 files covered. (81.82%)

4 existing lines in 2 files now uncovered.

22708 of 26237 relevant lines covered (86.55%)

219.92 hits per line

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

97.38
/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;
643✔
371

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

377
            if (
378
                in_array($setting, self::DIRECTIVES_ALLOWING_SOURCE_LISTS, true)
643✔
379
                && is_array($value)
643✔
380
                && array_is_list($value)
643✔
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));
643✔
385

386
                continue;
643✔
387
            }
388

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

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

396
        if (! is_array($this->scriptSrc)) {
643✔
397
            $this->scriptSrc = [$this->scriptSrc => $this->reportOnly];
643✔
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
     * Whether adding nonce in style-* directives is enabled or not.
411
     */
412
    public function styleNonceEnabled(): bool
413
    {
414
        return $this->enabled() && $this->enableStyleNonce;
5✔
415
    }
416

417
    /**
418
     * Whether adding nonce in script-* directives is enabled or not.
419
     */
420
    public function scriptNonceEnabled(): bool
421
    {
422
        return $this->enabled() && $this->enableScriptNonce;
24✔
423
    }
424

425
    /**
426
     * Get the nonce for the style tag.
427
     */
428
    public function getStyleNonce(): string
429
    {
430
        if (! $this->enableStyleNonce) {
11✔
NEW
431
            $this->styleNonce = null;
×
432

NEW
UNCOV
433
            return '';
×
434
        }
435

436
        if ($this->styleNonce === null) {
11✔
437
            $this->styleNonce = base64_encode(random_bytes(12));
11✔
438
            $this->addStyleSrc('nonce-' . $this->styleNonce);
11✔
439

440
            if ($this->styleSrcElem !== []) {
11✔
441
                $this->addStyleSrcElem('nonce-' . $this->styleNonce);
10✔
442
            }
443
        }
444

445
        return $this->styleNonce;
11✔
446
    }
447

448
    /**
449
     * Get the nonce for the script tag.
450
     */
451
    public function getScriptNonce(): string
452
    {
453
        if (! $this->enableScriptNonce) {
14✔
NEW
454
            $this->scriptNonce = null;
×
455

NEW
UNCOV
456
            return '';
×
457
        }
458

459
        if ($this->scriptNonce === null) {
14✔
460
            $this->scriptNonce = base64_encode(random_bytes(12));
14✔
461
            $this->addScriptSrc('nonce-' . $this->scriptNonce);
14✔
462

463
            if ($this->scriptSrcElem !== []) {
14✔
464
                $this->addScriptSrcElem('nonce-' . $this->scriptNonce);
13✔
465
            }
466
        }
467

468
        return $this->scriptNonce;
14✔
469
    }
470

471
    /**
472
     * Compiles and sets the appropriate headers in the request.
473
     *
474
     * Should be called just prior to sending the response to the user agent.
475
     *
476
     * @return void
477
     */
478
    public function finalize(ResponseInterface $response)
479
    {
480
        $this->generateNonces($response);
127✔
481

482
        $this->buildHeaders($response);
127✔
483
    }
484

485
    /**
486
     * If TRUE, nothing will be restricted. Instead all violations will
487
     * be reported to the reportURI for monitoring. This is useful when
488
     * you are just starting to implement the policy, and will help
489
     * determine what errors need to be addressed before you turn on
490
     * all filtering.
491
     *
492
     * @return $this
493
     */
494
    public function reportOnly(bool $value = true)
495
    {
496
        $this->reportOnly = $value;
10✔
497

498
        return $this;
10✔
499
    }
500

501
    /**
502
     * Adds a new value to the `base-uri` directive.
503
     *
504
     * `base-uri` restricts the URLs that can appear in a page's <base> element.
505
     *
506
     * @see http://www.w3.org/TR/CSP/#directive-base-uri
507
     *
508
     * @param list<string>|string $uri
509
     *
510
     * @return $this
511
     */
512
    public function addBaseURI($uri, ?bool $explicitReporting = null)
513
    {
514
        $this->addOption($uri, 'baseURI', $explicitReporting ?? $this->reportOnly);
3✔
515

516
        return $this;
3✔
517
    }
518

519
    /**
520
     * Adds a new value to the `child-src` directive.
521
     *
522
     * `child-src` lists the URLs for workers and embedded frame contents.
523
     * For example: child-src https://youtube.com would enable embedding
524
     * videos from YouTube but not from other origins.
525
     *
526
     * @see http://www.w3.org/TR/CSP/#directive-child-src
527
     *
528
     * @param list<string>|string $uri
529
     *
530
     * @return $this
531
     */
532
    public function addChildSrc($uri, ?bool $explicitReporting = null)
533
    {
534
        $this->addOption($uri, 'childSrc', $explicitReporting ?? $this->reportOnly);
2✔
535

536
        return $this;
2✔
537
    }
538

539
    /**
540
     * Adds a new value to the `connect-src` directive.
541
     *
542
     * `connect-src` limits the origins to which you can connect
543
     * (via XHR, WebSockets, and EventSource).
544
     *
545
     * @see http://www.w3.org/TR/CSP/#directive-connect-src
546
     *
547
     * @param list<string>|string $uri
548
     *
549
     * @return $this
550
     */
551
    public function addConnectSrc($uri, ?bool $explicitReporting = null)
552
    {
553
        $this->addOption($uri, 'connectSrc', $explicitReporting ?? $this->reportOnly);
1✔
554

555
        return $this;
1✔
556
    }
557

558
    /**
559
     * Adds a new value to the `default-src` directive.
560
     *
561
     * `default-src` is the URI that is used for many of the settings when
562
     * no other source has been set.
563
     *
564
     * @see http://www.w3.org/TR/CSP/#directive-default-src
565
     *
566
     * @param list<string>|string $uri
567
     *
568
     * @return $this
569
     */
570
    public function setDefaultSrc($uri, ?bool $explicitReporting = null)
571
    {
572
        $this->defaultSrc = [(string) $uri => $explicitReporting ?? $this->reportOnly];
1✔
573

574
        return $this;
1✔
575
    }
576

577
    /**
578
     * Adds a new value to the `font-src` directive.
579
     *
580
     * `font-src` specifies the origins that can serve web fonts.
581
     *
582
     * @see http://www.w3.org/TR/CSP/#directive-font-src
583
     *
584
     * @param list<string>|string $uri
585
     *
586
     * @return $this
587
     */
588
    public function addFontSrc($uri, ?bool $explicitReporting = null)
589
    {
590
        $this->addOption($uri, 'fontSrc', $explicitReporting ?? $this->reportOnly);
3✔
591

592
        return $this;
3✔
593
    }
594

595
    /**
596
     * Adds a new value to the `form-action` directive.
597
     *
598
     * @see http://www.w3.org/TR/CSP/#directive-form-action
599
     *
600
     * @param list<string>|string $uri
601
     *
602
     * @return $this
603
     */
604
    public function addFormAction($uri, ?bool $explicitReporting = null)
605
    {
606
        $this->addOption($uri, 'formAction', $explicitReporting ?? $this->reportOnly);
2✔
607

608
        return $this;
2✔
609
    }
610

611
    /**
612
     * Adds a new value to the `frame-ancestors` directive.
613
     *
614
     * @see http://www.w3.org/TR/CSP/#directive-frame-ancestors
615
     *
616
     * @param list<string>|string $uri
617
     *
618
     * @return $this
619
     */
620
    public function addFrameAncestor($uri, ?bool $explicitReporting = null)
621
    {
622
        $this->addOption($uri, 'frameAncestors', $explicitReporting ?? $this->reportOnly);
2✔
623

624
        return $this;
2✔
625
    }
626

627
    /**
628
     * Adds a new value to the `frame-src` directive.
629
     *
630
     * @see http://www.w3.org/TR/CSP/#directive-frame-src
631
     *
632
     * @param list<string>|string $uri
633
     *
634
     * @return $this
635
     */
636
    public function addFrameSrc($uri, ?bool $explicitReporting = null)
637
    {
638
        $this->addOption($uri, 'frameSrc', $explicitReporting ?? $this->reportOnly);
2✔
639

640
        return $this;
2✔
641
    }
642

643
    /**
644
     * Adds a new value to the `img-src` directive.
645
     *
646
     * @see http://www.w3.org/TR/CSP/#directive-img-src
647
     *
648
     * @param list<string>|string $uri
649
     *
650
     * @return $this
651
     */
652
    public function addImageSrc($uri, ?bool $explicitReporting = null)
653
    {
654
        $this->addOption($uri, 'imageSrc', $explicitReporting ?? $this->reportOnly);
5✔
655

656
        return $this;
5✔
657
    }
658

659
    /**
660
     * Adds a new value to the `media-src` directive.
661
     *
662
     * @see http://www.w3.org/TR/CSP/#directive-media-src
663
     *
664
     * @param list<string>|string $uri
665
     *
666
     * @return $this
667
     */
668
    public function addMediaSrc($uri, ?bool $explicitReporting = null)
669
    {
670
        $this->addOption($uri, 'mediaSrc', $explicitReporting ?? $this->reportOnly);
2✔
671

672
        return $this;
2✔
673
    }
674

675
    /**
676
     * Adds a new value to the `manifest-src` directive.
677
     *
678
     * @see https://www.w3.org/TR/CSP/#directive-manifest-src
679
     *
680
     * @param list<string>|string $uri
681
     *
682
     * @return $this
683
     */
684
    public function addManifestSrc($uri, ?bool $explicitReporting = null)
685
    {
686
        $this->addOption($uri, 'manifestSrc', $explicitReporting ?? $this->reportOnly);
2✔
687

688
        return $this;
2✔
689
    }
690

691
    /**
692
     * Adds a new value to the `object-src` directive.
693
     *
694
     * @see http://www.w3.org/TR/CSP/#directive-object-src
695
     *
696
     * @param list<string>|string $uri
697
     *
698
     * @return $this
699
     */
700
    public function addObjectSrc($uri, ?bool $explicitReporting = null)
701
    {
702
        $this->addOption($uri, 'objectSrc', $explicitReporting ?? $this->reportOnly);
2✔
703

704
        return $this;
2✔
705
    }
706

707
    /**
708
     * Adds a new value to the `plugin-types` directive.
709
     *
710
     * @see http://www.w3.org/TR/CSP/#directive-plugin-types
711
     *
712
     * @param list<string>|string $mime
713
     *
714
     * @return $this
715
     */
716
    public function addPluginType($mime, ?bool $explicitReporting = null)
717
    {
718
        $this->addOption($mime, 'pluginTypes', $explicitReporting ?? $this->reportOnly);
2✔
719

720
        return $this;
2✔
721
    }
722

723
    /**
724
     * Adds a new value to the `sandbox` directive.
725
     *
726
     * @see http://www.w3.org/TR/CSP/#directive-sandbox
727
     *
728
     * @param list<string>|string $flags
729
     *
730
     * @return $this
731
     */
732
    public function addSandbox($flags, ?bool $explicitReporting = null)
733
    {
734
        $this->addOption($flags, 'sandbox', $explicitReporting ?? $this->reportOnly);
2✔
735

736
        return $this;
2✔
737
    }
738

739
    /**
740
     * Adds a new value to the `script-src` directive.
741
     *
742
     * @see http://www.w3.org/TR/CSP/#directive-script-src
743
     *
744
     * @param list<string>|string $uri
745
     *
746
     * @return $this
747
     */
748
    public function addScriptSrc($uri, ?bool $explicitReporting = null)
749
    {
750
        $this->addOption($uri, 'scriptSrc', $explicitReporting ?? $this->reportOnly);
18✔
751

752
        return $this;
18✔
753
    }
754

755
    /**
756
     * Adds a new value to the `script-src-elem` directive.
757
     *
758
     * @see https://www.w3.org/TR/CSP/#directive-script-src-elem
759
     *
760
     * @param list<string>|string $uri
761
     */
762
    public function addScriptSrcElem(array|string $uri, ?bool $explicitReporting = null): static
763
    {
764
        $this->addOption($uri, 'scriptSrcElem', $explicitReporting ?? $this->reportOnly);
14✔
765

766
        return $this;
14✔
767
    }
768

769
    /**
770
     * Adds a new value to the `script-src-attr` directive.
771
     *
772
     * @see https://www.w3.org/TR/CSP/#directive-script-src-attr
773
     *
774
     * @param list<string>|string $uri
775
     */
776
    public function addScriptSrcAttr(array|string $uri, ?bool $explicitReporting = null): static
777
    {
778
        $this->addOption($uri, 'scriptSrcAttr', $explicitReporting ?? $this->reportOnly);
1✔
779

780
        return $this;
1✔
781
    }
782

783
    /**
784
     * Adds a new value to the `style-src` directive.
785
     *
786
     * @see http://www.w3.org/TR/CSP/#directive-style-src
787
     *
788
     * @param list<string>|string $uri
789
     *
790
     * @return $this
791
     */
792
    public function addStyleSrc($uri, ?bool $explicitReporting = null)
793
    {
794
        $this->addOption($uri, 'styleSrc', $explicitReporting ?? $this->reportOnly);
16✔
795

796
        return $this;
16✔
797
    }
798

799
    /**
800
     * Adds a new value to the `style-src-elem` directive.
801
     *
802
     * @see https://www.w3.org/TR/CSP/#directive-style-src-elem
803
     *
804
     * @param list<string>|string $uri
805
     */
806
    public function addStyleSrcElem(array|string $uri, ?bool $explicitReporting = null): static
807
    {
808
        $this->addOption($uri, 'styleSrcElem', $explicitReporting ?? $this->reportOnly);
11✔
809

810
        return $this;
11✔
811
    }
812

813
    /**
814
     * Adds a new value to the `style-src-attr` directive.
815
     *
816
     * @see https://www.w3.org/TR/CSP/#directive-style-src-attr
817
     *
818
     * @param list<string>|string $uri
819
     */
820
    public function addStyleSrcAttr(array|string $uri, ?bool $explicitReporting = null): static
821
    {
822
        $this->addOption($uri, 'styleSrcAttr', $explicitReporting ?? $this->reportOnly);
1✔
823

824
        return $this;
1✔
825
    }
826

827
    /**
828
     * Adds a new value to the `worker-src` directive.
829
     *
830
     * @see https://www.w3.org/TR/CSP/#directive-worker-src
831
     *
832
     * @param list<string>|string $uri
833
     */
834
    public function addWorkerSrc($uri, ?bool $explicitReporting = null): static
835
    {
836
        $this->addOption($uri, 'workerSrc', $explicitReporting ?? $this->reportOnly);
1✔
837

838
        return $this;
1✔
839
    }
840

841
    /**
842
     * Sets whether the user agents should rewrite URL schemes, changing HTTP to HTTPS.
843
     *
844
     * @return $this
845
     */
846
    public function upgradeInsecureRequests(bool $value = true)
847
    {
848
        $this->upgradeInsecureRequests = $value;
1✔
849

850
        return $this;
1✔
851
    }
852

853
    /**
854
     * Specifies a URL where a browser will send reports when a content
855
     * security policy is violated.
856
     *
857
     * @see http://www.w3.org/TR/CSP/#directive-report-uri
858
     *
859
     * @param string $uri URL to send reports. Set `''` if you want to remove
860
     *                    this directive at runtime.
861
     *
862
     * @return $this
863
     */
864
    public function setReportURI(string $uri)
865
    {
866
        $this->reportURI = $uri;
3✔
867

868
        return $this;
3✔
869
    }
870

871
    /**
872
     * Specifies a named group in a Reporting API endpoint to which the user
873
     * agent sends reports about policy violation.
874
     *
875
     * @see https://www.w3.org/TR/CSP/#directive-report-to
876
     *
877
     * @param string $endpoint The name of the reporting endpoint. Set `''` if you
878
     *                         want to remove this directive at runtime.
879
     */
880
    public function setReportToEndpoint(string $endpoint): static
881
    {
882
        if ($endpoint === '') {
5✔
883
            $this->reportURI = null;
1✔
884
            $this->reportTo  = null;
1✔
885

886
            return $this;
1✔
887
        }
888

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

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

896
        return $this;
4✔
897
    }
898

899
    /**
900
     * Adds reporting endpoints to the `Reporting-Endpoints` header.
901
     *
902
     * @param array<string, string> $endpoint
903
     */
904
    public function addReportingEndpoints(array $endpoint): static
905
    {
906
        foreach ($endpoint as $name => $url) {
4✔
907
            $this->reportingEndpoints[$name] = $url;
4✔
908
        }
909

910
        return $this;
4✔
911
    }
912

913
    /**
914
     * Enables or disables adding nonces to style-src and style-src-elem directives.
915
     *
916
     * @return $this
917
     */
918
    public function setEnableStyleNonce(bool $value = true): static
919
    {
920
        $this->enableStyleNonce = $value;
1✔
921

922
        return $this;
1✔
923
    }
924

925
    /**
926
     * Enables or disables adding nonces to script-src and script-src-elem directives.
927
     *
928
     * @return $this
929
     */
930
    public function setEnableScriptNonce(bool $value = true): static
931
    {
932
        $this->enableScriptNonce = $value;
1✔
933

934
        return $this;
1✔
935
    }
936

937
    /**
938
     * DRY method to add an string or array to a class property.
939
     *
940
     * @param list<string>|string $options
941
     *
942
     * @return void
943
     */
944
    protected function addOption($options, string $target, ?bool $explicitReporting = null)
945
    {
946
        // Ensure we have an array to work with...
947
        if (is_string($this->{$target})) {
54✔
948
            $this->{$target} = [$this->{$target} => $this->reportOnly];
30✔
949
        }
950

951
        $options = is_array($options) ? $options : [$options];
54✔
952

953
        foreach ($options as $option) {
54✔
954
            $this->{$target}[$option] = $explicitReporting ?? $this->reportOnly;
54✔
955
        }
956
    }
957

958
    /**
959
     * Scans the body of the request message and replaces any nonce
960
     * placeholders with actual nonces, that we'll then add to our
961
     * headers.
962
     *
963
     * @return void
964
     */
965
    protected function generateNonces(ResponseInterface $response)
966
    {
967
        if ($this->enabled() && ! $this->autoNonce) {
127✔
968
            return;
2✔
969
        }
970

971
        $body = (string) $response->getBody();
125✔
972

973
        if ($body === '') {
125✔
974
            return;
15✔
975
        }
976

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

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

983
        $body = preg_replace_callback($pattern, function ($match) use ($jsonEscape): string {
110✔
984
            if (! $this->enabled()) {
22✔
985
                return '';
14✔
986
            }
987

988
            if ($match[0] === $this->styleNonceTag) {
8✔
989
                if (! $this->enableStyleNonce) {
4✔
990
                    return '';
1✔
991
                }
992

993
                $nonce = $this->getStyleNonce();
3✔
994
            } else {
995
                if (! $this->enableScriptNonce) {
5✔
996
                    return '';
1✔
997
                }
998

999
                $nonce = $this->getScriptNonce();
4✔
1000
            }
1001

1002
            $attr = 'nonce="' . $nonce . '"';
6✔
1003

1004
            return $jsonEscape ? str_replace('"', '\\"', $attr) : $attr;
6✔
1005
        }, $body);
110✔
1006

1007
        $response->setBody($body);
110✔
1008
    }
1009

1010
    /**
1011
     * Based on the current state of the elements, will add the appropriate
1012
     * Content-Security-Policy and Content-Security-Policy-Report-Only headers
1013
     * with their values to the response object.
1014
     *
1015
     * @return void
1016
     */
1017
    protected function buildHeaders(ResponseInterface $response)
1018
    {
1019
        if (! $this->enabled()) {
127✔
1020
            return;
70✔
1021
        }
1022

1023
        $response->setHeader('Content-Security-Policy', []);
57✔
1024
        $response->setHeader('Content-Security-Policy-Report-Only', []);
57✔
1025
        $response->setHeader('Reporting-Endpoints', []);
57✔
1026

1027
        if (in_array($this->baseURI, ['', null, []], true)) {
57✔
1028
            $this->baseURI = 'self';
54✔
1029
        }
1030

1031
        if (in_array($this->defaultSrc, ['', null, []], true)) {
57✔
1032
            $this->defaultSrc = 'self';
55✔
1033
        }
1034

1035
        foreach ($this->directives as $name => $property) {
57✔
1036
            if ($name === 'report-uri' && (string) $this->reportURI === '') {
57✔
1037
                continue;
54✔
1038
            }
1039

1040
            if ($name === 'report-to' && (string) $this->reportTo === '') {
57✔
1041
                continue;
55✔
1042
            }
1043

1044
            if ($this->{$property} !== null) {
57✔
1045
                $this->addToHeader($name, $this->{$property});
57✔
1046
            }
1047
        }
1048

1049
        // Compile our own header strings here since if we just
1050
        // append it to the response, it will be joined with
1051
        // commas, not semi-colons as we need.
1052
        if ($this->reportingEndpoints !== []) {
57✔
1053
            $endpoints = [];
4✔
1054

1055
            foreach ($this->reportingEndpoints as $name => $url) {
4✔
1056
                $endpoints[] = trim("{$name}=\"{$url}\"");
4✔
1057
            }
1058

1059
            $response->appendHeader('Reporting-Endpoints', implode(', ', $endpoints));
4✔
1060
            $this->reportingEndpoints = [];
4✔
1061
        }
1062

1063
        if ($this->tempHeaders !== []) {
57✔
1064
            $header = [];
57✔
1065

1066
            foreach ($this->tempHeaders as $name => $value) {
57✔
1067
                $header[] = trim("{$name} {$value}");
57✔
1068
            }
1069

1070
            if ($this->upgradeInsecureRequests) {
57✔
1071
                $header[] = 'upgrade-insecure-requests';
1✔
1072
            }
1073

1074
            $response->appendHeader('Content-Security-Policy', implode('; ', $header));
57✔
1075
            $this->tempHeaders = [];
57✔
1076
        }
1077

1078
        if ($this->reportOnlyHeaders !== []) {
57✔
1079
            $header = [];
17✔
1080

1081
            foreach ($this->reportOnlyHeaders as $name => $value) {
17✔
1082
                $header[] = trim("{$name} {$value}");
17✔
1083
            }
1084

1085
            $response->appendHeader('Content-Security-Policy-Report-Only', implode('; ', $header));
17✔
1086
            $this->reportOnlyHeaders = [];
17✔
1087
        }
1088
    }
1089

1090
    /**
1091
     * Adds a directive and its options to the appropriate header. The $values
1092
     * array might have options that are geared toward either the regular or the
1093
     * reportOnly header, since it's viable to have both simultaneously.
1094
     *
1095
     * @param array<string, bool>|string $values
1096
     *
1097
     * @return void
1098
     */
1099
    protected function addToHeader(string $name, $values = null)
1100
    {
1101
        if (is_string($values)) {
57✔
1102
            $values = [$values => $this->reportOnly];
57✔
1103
        }
1104

1105
        $sources       = [];
57✔
1106
        $reportSources = [];
57✔
1107

1108
        foreach ($values as $value => $reportOnly) {
57✔
1109
            if (
1110
                in_array($value, $this->validSources, true)
57✔
1111
                || str_starts_with($value, 'nonce-')
43✔
1112
                || str_starts_with($value, 'sha256-')
36✔
1113
                || str_starts_with($value, 'sha384-')
36✔
1114
                || str_starts_with($value, 'sha512-')
57✔
1115
            ) {
1116
                $value = "'{$value}'";
57✔
1117
            }
1118

1119
            if ($reportOnly) {
57✔
1120
                $reportSources[] = $value;
17✔
1121
            } else {
1122
                $sources[] = $value;
57✔
1123
            }
1124
        }
1125

1126
        if ($sources !== []) {
57✔
1127
            $this->tempHeaders[$name] = implode(' ', $sources);
57✔
1128
        }
1129

1130
        if ($reportSources !== []) {
57✔
1131
            $this->reportOnlyHeaders[$name] = implode(' ', $reportSources);
17✔
1132
        }
1133
    }
1134

1135
    public function clearDirective(string $directive): void
1136
    {
1137
        if (! array_key_exists($directive, $this->directives)) {
8✔
1138
            return;
1✔
1139
        }
1140

1141
        if ($directive === 'report-uri') {
8✔
1142
            $this->reportURI = null;
1✔
1143

1144
            return;
1✔
1145
        }
1146

1147
        if ($directive === 'report-to') {
8✔
1148
            $this->reportURI = null;
1✔
1149
            $this->reportTo  = null;
1✔
1150

1151
            return;
1✔
1152
        }
1153

1154
        $this->{$this->directives[$directive]} = [];
8✔
1155
    }
1156
}
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