Skip to content

Commit 00b7d15

Browse files
fix: harden CSP policy and address PR review feedback
1 parent aba3526 commit 00b7d15

6 files changed

Lines changed: 23 additions & 12 deletions

File tree

app.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
"img-src 'self' data:",
2626
"connect-src 'self'",
2727
"font-src 'self'",
28+
"object-src 'none'",
29+
"form-action 'self'",
2830
"base-uri 'self'",
2931
"frame-ancestors 'none'",
3032
]
@@ -102,7 +104,8 @@ def create_app(
102104

103105
@app.after_request
104106
def set_security_headers(response):
105-
response.headers.setdefault("Content-Security-Policy", CSP_POLICY)
107+
# Always set — do not use setdefault; a blueprint must not weaken CSP.
108+
response.headers["Content-Security-Policy"] = CSP_POLICY
106109
return response
107110

108111
@app.route("/")

docs/architecture.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ No bundler step — modern browsers load modules directly. Frontend unit tests u
125125
| `img-src` | `'self'`, `data:` | Session images and data URLs |
126126
| `connect-src` | `'self'` | API `fetch` calls to same origin |
127127
| `font-src` | `'self'` | Local fonts only |
128+
| `object-src` | `'none'` | Block plugins / `<object>` embeds (no plugin use in this app) |
129+
| `form-action` | `'self'` | Restrict form submissions to same origin |
128130
| `base-uri` | `'self'` | Restrict `<base>` tag injection |
129131
| `frame-ancestors` | `'none'` | Prevent clickjacking via iframes |
130132

static/js/app.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
// Shared helpers live in shared/utils.js, shared/markdown.js, shared/theme.js.
44

55
import { state } from './shared/state.js';
6-
import { toggleSidebar, closeSidebar, loadingBar } from './shared/utils.js';
7-
import { HLJS_THEME_SHEETS, applyHljsTheme, applyTheme, toggleTheme, setWorkspaceMode } from './shared/theme.js';
6+
import { toggleSidebar, closeSidebar } from './shared/utils.js';
7+
import { HLJS_THEME_SHEETS, applyTheme, toggleTheme } from './shared/theme.js';
88
import { showProjects } from './projects.js';
99
import { showWorkspace, loadSession } from './sessions.js';
1010
import { showSearchPage } from './search.js';

static/js/search.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export function showSearchPage() {
4444
if (!project || !sessionId) return;
4545
window.location.hash = `#project/${encodeURIComponent(project)}/${encodeURIComponent(sessionId)}`;
4646
});
47-
document.getElementById('search-input').focus();
47+
document.getElementById('search-input')?.focus();
4848
}
4949

5050
export async function doSearch() {

tests/test_api_integration.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ def test_root_sets_csp_header(client):
2121
assert resp.headers.get("Content-Security-Policy") == CSP_POLICY
2222

2323

24+
def test_api_routes_set_csp_header(client):
25+
resp = client.get("/api/projects")
26+
assert resp.status_code == 200
27+
assert resp.headers.get("Content-Security-Policy") == CSP_POLICY
28+
29+
2430
# --- /api/projects ---
2531

2632

tests/test_hljs_theme_consistency.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
HLJS_THEME_INIT_JS = REPO_ROOT / "static" / "js" / "hljs-theme-init.js"
2020
# HLJS_THEME_SHEETS was extracted to shared/theme.js (Day 4 module split).
2121
# app.js re-exports it, but the canonical source is theme.js.
22-
APP_JS = REPO_ROOT / "static" / "js" / "shared" / "theme.js"
22+
THEME_JS = REPO_ROOT / "static" / "js" / "shared" / "theme.js"
2323

2424

2525
def _link_attr(html: str, link_id: str, attr: str) -> str:
@@ -38,7 +38,7 @@ def _link_attr(html: str, link_id: str, attr: str) -> str:
3838
def _js_theme_entry(js: str, theme: str) -> dict:
3939
"""Return {'href': ..., 'integrity': ...} from HLJS_THEME_SHEETS.<theme>."""
4040
block = re.search(re.escape(theme) + r"\s*:\s*\{([^}]*)\}", js, re.DOTALL)
41-
assert block, f"HLJS_THEME_SHEETS.{theme} entry not found in app.js"
41+
assert block, f"HLJS_THEME_SHEETS.{theme} entry not found in theme.js"
4242
body = block.group(1)
4343
out = {}
4444
for key in ("href", "integrity"):
@@ -63,27 +63,27 @@ def _js_string_assignments(js: str, keys: tuple[str, ...]) -> dict[str, str]:
6363

6464
def test_dark_theme_url_and_hash_match_between_html_and_js():
6565
html = INDEX_HTML.read_text(encoding="utf-8")
66-
js = APP_JS.read_text(encoding="utf-8")
66+
js = THEME_JS.read_text(encoding="utf-8")
6767

6868
html_href = _link_attr(html, "hljs-theme", "href")
6969
html_integrity = _link_attr(html, "hljs-theme", "integrity")
7070
js_dark = _js_theme_entry(js, "dark")
7171

7272
assert html_href == js_dark["href"], (
73-
"highlight.js theme URL drifted between index.html and app.js — "
74-
f"html={html_href!r}, app.js HLJS_THEME_SHEETS.dark={js_dark['href']!r}. "
73+
"highlight.js theme URL drifted between index.html and theme.js — "
74+
f"html={html_href!r}, theme.js HLJS_THEME_SHEETS.dark={js_dark['href']!r}. "
7575
"On a version bump both must update together (issue #19)."
7676
)
7777
assert html_integrity == js_dark["integrity"], (
78-
"highlight.js theme SRI hash drifted between index.html and app.js — "
78+
"highlight.js theme SRI hash drifted between index.html and theme.js — "
7979
f"html={html_integrity!r}, "
80-
f"app.js HLJS_THEME_SHEETS.dark={js_dark['integrity']!r}."
80+
f"theme.js HLJS_THEME_SHEETS.dark={js_dark['integrity']!r}."
8181
)
8282

8383

8484
def test_light_theme_url_and_hash_match_between_hljs_init_and_theme_js():
8585
init_js = HLJS_THEME_INIT_JS.read_text(encoding="utf-8")
86-
theme_js = APP_JS.read_text(encoding="utf-8")
86+
theme_js = THEME_JS.read_text(encoding="utf-8")
8787

8888
init = _js_string_assignments(init_js, ("integrity", "href"))
8989
js_light = _js_theme_entry(theme_js, "light")

0 commit comments

Comments
 (0)