44
55import re
66
7- from app import build_content_security_policy
7+ _CDNJS_ORIGIN = "https://cdnjs.cloudflare.com"
8+ _EXPECTED_CSP_DIRECTIVES = {
9+ "default-src" : ["'self'" ],
10+ "script-src" : ["'self'" , _CDNJS_ORIGIN ],
11+ "style-src" : ["'self'" , "'unsafe-inline'" , _CDNJS_ORIGIN ],
12+ "img-src" : ["'self'" , "data:" ],
13+ "connect-src" : ["'self'" ],
14+ "font-src" : ["'self'" ],
15+ "object-src" : ["'none'" ],
16+ "form-action" : ["'self'" ],
17+ "base-uri" : ["'self'" ],
18+ "frame-ancestors" : ["'none'" ],
19+ }
20+
21+
22+ def _parse_csp_directives (csp : str ) -> dict [str , list [str ]]:
23+ directives : dict [str , list [str ]] = {}
24+ for part in csp .split (";" ):
25+ part = part .strip ()
26+ if not part :
27+ continue
28+ name , _ , value = part .partition (" " )
29+ directives [name ] = value .split ()
30+ return directives
831
932
1033def _extract_csp_nonce (csp : str ) -> str :
@@ -13,25 +36,55 @@ def _extract_csp_nonce(csp: str) -> str:
1336 return match .group (1 )
1437
1538
39+ def _assert_expected_csp_directives (csp : str , nonce : str ) -> None :
40+ directives = _parse_csp_directives (csp )
41+ assert set (directives ) == set (_EXPECTED_CSP_DIRECTIVES )
42+
43+ for name , expected_tokens in _EXPECTED_CSP_DIRECTIVES .items ():
44+ actual_tokens = directives [name ]
45+ if name == "script-src" :
46+ nonce_tokens = [
47+ token for token in actual_tokens if token .startswith ("'nonce-" )
48+ ]
49+ non_nonce_tokens = [
50+ token for token in actual_tokens if not token .startswith ("'nonce-" )
51+ ]
52+ assert non_nonce_tokens == expected_tokens
53+ assert len (nonce_tokens ) == 1
54+ assert nonce_tokens [0 ] == f"'nonce-{ nonce } '"
55+ else :
56+ assert actual_tokens == expected_tokens
57+
58+
59+ def _assert_inline_script_nonce (html : str , nonce : str ) -> None :
60+ pattern = rf'<script\s+nonce="{ re .escape (nonce )} "'
61+ assert re .search (pattern , html ) is not None
62+
63+
1664def test_html_page_has_content_security_policy_header (client ) -> None :
1765 response = client .get ("/" )
1866 assert response .status_code == 200
1967 csp = response .headers .get ("Content-Security-Policy" )
2068 assert csp
21- assert "default-src 'self'" in csp
22- assert "script-src 'self' https://cdnjs.cloudflare.com" in csp
23- assert "style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com" in csp
24- assert "'nonce-" in csp
69+ nonce = _extract_csp_nonce (csp )
70+ _assert_expected_csp_directives (csp , nonce )
2571
2672
2773def test_html_page_nonce_matches_inline_script (client ) -> None :
28- response = client .get ("/search" )
29- assert response .status_code == 200
30- csp = response .headers .get ("Content-Security-Policy" , "" )
31- nonce = _extract_csp_nonce (csp )
32- html = response .get_data (as_text = True )
33- assert f'nonce="{ nonce } "' in html
34- assert build_content_security_policy (nonce ) == csp
74+ first = client .get ("/search" )
75+ second = client .get ("/search" )
76+ assert first .status_code == 200
77+ assert second .status_code == 200
78+
79+ first_csp = first .headers .get ("Content-Security-Policy" , "" )
80+ second_csp = second .headers .get ("Content-Security-Policy" , "" )
81+ first_nonce = _extract_csp_nonce (first_csp )
82+ second_nonce = _extract_csp_nonce (second_csp )
83+ assert first_nonce != second_nonce
84+
85+ html = first .get_data (as_text = True )
86+ _assert_expected_csp_directives (first_csp , first_nonce )
87+ _assert_inline_script_nonce (html , first_nonce )
3588
3689
3790def test_json_api_response_has_no_content_security_policy_header (client ) -> None :
0 commit comments