harden: stream-token host allowlist (SSRF), CSRF header on mutating routes, atomic JSON writes

- /stream/{token}: tokens are plain base64; allowlist https://hls.anidb.app
  so a session can't use the proxy for arbitrary URLs (new test)
- X-Aniweb:1 required on POST/DELETE except /login — SameSite=Lax is
  still sent on cross-site form POSTs, which could hit /account/delete
  (new test; SPA + tests send the header)
- tmp+os.replace writes for users/history/searches: a crash mid-write
  could corrupt the JSON -> load() returned {} -> silent data loss
- resume positions now read from full history (?all=1), not top-5
- pagehide bound once; savePos reads currentEp (no stale closures)
- account delete also removes the searches file; episodes TTL 5m
- ani-web.service: supervised restart (not installed; see header)
- gitignore pi-session dumps + .pytest_cache
This commit is contained in:
jozamudi committed 2026-08-23 23:22:09 +00:00
1 parent d39ee838ff
commit aae5857dba
8 files changed
+122 -32

No files matched your search

+3 -3
View File
@@ -120,7 +120,7 @@ async def _history_page_and_delete():
"lang": "eng", "ep": n, "pos": 5, "dur": 600}
op.open(urllib.request.Request(BASE + "/api/history",
data=json.dumps(e).encode(),
headers={"Content-Type": "application/json"}, method="POST"))
headers={"Content-Type": "application/json", "X-Aniweb": "1"}, method="POST"))
async with async_playwright() as p:
browser = await p.chromium.launch(args=LAUNCH_ARGS)
@@ -190,7 +190,7 @@ async def _recent_searches():
headers={"Content-Type": "application/json"}, method="POST"))
op.open(urllib.request.Request(BASE + "/api/searches",
data=json.dumps({"q": "chainsaw"}).encode(),
headers={"Content-Type": "application/json"}, method="POST"))
headers={"Content-Type": "application/json", "X-Aniweb": "1"}, method="POST"))
async with async_playwright() as p:
browser = await p.chromium.launch(args=LAUNCH_ARGS)
@@ -268,7 +268,7 @@ async def _episode_watch_state():
e1_id = await page.evaluate("document.querySelector('#eps .ep').dataset.ep")
await page.evaluate(
"(id) => fetch('api/history', {method: 'POST',"
" headers: {'Content-Type': 'application/json'},"
" headers: {'Content-Type': 'application/json', 'X-Aniweb': '1'},"
" body: JSON.stringify({title: 'Chainsaw Man', slug_id: 'chainsaw-man-922',"
" season_id: 922, ep_id: +id, lang: 'eng', ep: 1, pos: 2350, dur: 2400})})",
e1_id)