Skip to content
This repository was archived by the owner on Jul 9, 2024. It is now read-only.

Commit cafe829

Browse files
committed
Add support for hashes to the script-src directive
1 parent 615d230 commit cafe829

File tree

4 files changed

+175
-0
lines changed

4 files changed

+175
-0
lines changed

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ Your HTML should look like this:
150150

151151
The nonce should be random for each request so attackers cannot predict the nonce value.
152152

153+
### Hashes
154+
If your application requires inline scripts you can serve the SHA256, SHA384, or SHA512 hash of the source as part of the script-src directive in your policy to allow the script to run. This way you don't need to enable unsafe-inline.
155+
153156
### Violation reports
154157
CSP gives you the option to receive reports about CSP violations. Each time a page loads a resource that is blocked by your CSP policy, the browser will submit a JSON object to the URL you specified in your policy. In the following example, those report will be send to `https://example.com/csp/report.php`:
155158

examples/example-hash.php

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
/**
3+
* Copyright 2015, Martijn Croonen.
4+
* All rights reserved.
5+
*
6+
* Use of this source code is governed by a BSD-style license that can be
7+
* found in the LICENSE file.
8+
*/
9+
include '../src/Phpcsp/Security/ContentSecurityPolicyHeaderBuilder.php';
10+
11+
use Phpcsp\Security\ContentSecurityPolicyHeaderBuilder;
12+
13+
$policy = new ContentSecurityPolicyHeaderBuilder();
14+
15+
// Set the default-src directive to 'none'
16+
$policy->addSourceExpression(ContentSecurityPolicyHeaderBuilder::DIRECTIVE_DEFAULT_SRC, 'none');
17+
18+
$script = "alert('Hello, world.');";
19+
20+
$policy->addHash(
21+
ContentSecurityPolicyHeaderBuilder::HASH_SHA_256,
22+
hash(ContentSecurityPolicyHeaderBuilder::HASH_SHA_256, $script, true)
23+
);
24+
25+
// Get your CSP headers
26+
$headers = $policy->getHeaders(false);
27+
foreach ($headers as $header) {
28+
header(sprintf('%s: %s', $header['name'], $header['value']));
29+
}
30+
?>
31+
32+
<html>
33+
<body>
34+
<!-- Script will work -->
35+
<script type="text/javascript"><?php echo $script; ?></script>
36+
</body>
37+
</html>

src/Phpcsp/Security/ContentSecurityPolicyHeaderBuilder.php

+62
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,38 @@ class ContentSecurityPolicyHeaderBuilder
308308
null
309309
];
310310

311+
/**
312+
* SHA 256 hash indicator.
313+
*
314+
* @var string
315+
*/
316+
const HASH_SHA_256 = 'sha256';
317+
318+
/**
319+
* SHA 384 hash indicator.
320+
*
321+
* @var string
322+
*/
323+
const HASH_SHA_384 = 'sha384';
324+
325+
/**
326+
* SHA 512 hash indicator.
327+
*
328+
* @var string
329+
*/
330+
const HASH_SHA_512 = 'sha512';
331+
332+
/**
333+
* Supported hash algorithms.
334+
*
335+
* @var array
336+
*/
337+
protected $hashAlgorithmValues = [
338+
self::HASH_SHA_256,
339+
self::HASH_SHA_384,
340+
self::HASH_SHA_512
341+
];
342+
311343
/**
312344
* Holds the value for the 'X-Frame-Options' header when set.
313345
*
@@ -510,6 +542,27 @@ public function addSourceExpression($directive, $expression)
510542
$this->directives[$directive]['expressions'][] = $expression;
511543
}
512544

545+
/**
546+
* Add a hash value to the script-src.
547+
*
548+
* @param string $type
549+
* @param string $hash
550+
* @throws InvalidDirectiveException
551+
*/
552+
public function addHash($type, $hash)
553+
{
554+
$directive = self::DIRECTIVE_SCRIPT_SRC;
555+
if (!(isset($this->directives[$directive]) && is_array($this->directives[$directive]))) {
556+
$this->directives[$directive] = [];
557+
}
558+
559+
if (!(isset($this->directives[$directive]['hashes']) && is_array($this->directives[$directive]['hashes']))) {
560+
$this->directives[$directive]['hashes'] = [];
561+
}
562+
563+
$this->directives[$directive]['hashes'][$type][] = $hash;
564+
}
565+
513566
/**
514567
* Returns the CSP header that should be used (enforced mode or report-only mode).
515568
*
@@ -621,6 +674,15 @@ private function parseDirectiveValue($directive)
621674
}
622675
}
623676

677+
// Parse the hashes
678+
if (isset($directive['hashes']) && is_array($directive['hashes'])) {
679+
foreach ($directive['hashes'] as $type => $hashes) {
680+
foreach ($hashes as $hash) {
681+
$expressions[] = sprintf("'%s-%s'", $type, base64_encode($hash));
682+
}
683+
}
684+
}
685+
624686
return trim(implode(' ', array_map(function($value) {
625687
return $this->encodeDirectiveValue($value);
626688
}, $expressions)));

tests/Phpcsp/Security/ContentSecurityPolicyHeaderBuilderTest.php

+73
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,79 @@ public function testFrameOptionsValueAllowFrom()
304304
$this->assertEquals('ALLOW-FROM http://example.com', $headers[0]['value']);
305305
}
306306

307+
/**
308+
* Test for the SHA 256 hashing support.
309+
*
310+
* @throws InvalidDirectiveException
311+
*/
312+
public function testHashSha256()
313+
{
314+
$policy = $this->getNewInstance();
315+
316+
$script = "alert('Hello, world.');";
317+
318+
$policy->addHash(
319+
ContentSecurityPolicyHeaderBuilder::HASH_SHA_256,
320+
hash(ContentSecurityPolicyHeaderBuilder::HASH_SHA_256, $script, true)
321+
);
322+
323+
$headers = $policy->getHeaders(true);
324+
$this->assertEquals(1, count($headers));
325+
$this->assertEquals(
326+
'script-src \'sha256-qznLcsROx4GACP2dm0UCKCzCG+HiZ1guq6ZZDob/Tng=\';',
327+
$headers[0]['value']
328+
);
329+
}
330+
331+
/**
332+
* Test for the SHA 384 hashing support.
333+
*
334+
* @throws InvalidDirectiveException
335+
*/
336+
public function testHashSha384()
337+
{
338+
$policy = $this->getNewInstance();
339+
340+
$script = "alert('Hello, world.');";
341+
342+
$policy->addHash(
343+
ContentSecurityPolicyHeaderBuilder::HASH_SHA_384,
344+
hash(ContentSecurityPolicyHeaderBuilder::HASH_SHA_384, $script, true)
345+
);
346+
347+
$headers = $policy->getHeaders(true);
348+
$this->assertEquals(1, count($headers));
349+
$this->assertEquals(
350+
'script-src \'sha384-H8BRh8j48O9oYatfu5AZzq6A9RINhZO5H16dQZngK7T62em8MUt1FLm52t+eX6xO\';',
351+
$headers[0]['value']
352+
);
353+
}
354+
355+
/**
356+
* Test for the SHA 512 hashing support.
357+
*
358+
* @throws InvalidDirectiveException
359+
*/
360+
public function testHashSha512()
361+
{
362+
$policy = $this->getNewInstance();
363+
364+
$script = "alert('Hello, world.');";
365+
366+
$policy->addHash(
367+
ContentSecurityPolicyHeaderBuilder::HASH_SHA_512,
368+
hash(ContentSecurityPolicyHeaderBuilder::HASH_SHA_512, $script, true)
369+
);
370+
371+
$headers = $policy->getHeaders(true);
372+
$this->assertEquals(1, count($headers));
373+
$this->assertEquals(
374+
'script-src \'sha512-Q2bFTOhEALkN8hOms2FKTDLy7eugP2zFZ1T8LCvX42Fp3WoNr3bjZSAHeOsHrbV1Fu9/A0EzCinRE7Af1ofP' .
375+
'rw==\';',
376+
$headers[0]['value']
377+
);
378+
}
379+
307380
/**
308381
* Creates fresh instances of the helper.
309382
*

0 commit comments

Comments
 (0)