Small Control JSONs have arrived empty or wrong more than once, and the old check compared sizes only and skipped missing files. A copy of Deploy/Control on the dev PC still holds two 0-byte JSONs. Upload-Image.ps1 (tested on Windows PowerShell 5.1 in the win11 VM): - refuses an unfinished MCL cache: exactly one <OS>-Production.tag (MCL writes it last), no Control file newer than it, selected OS matches, every Control/Tools JSON non-empty and valid - checks payload against the SHA-256 MCL's own Control JSONs publish. MCL resumes downloads onto old files (FileMode.Append) and then trusts any file of the right size: the September junk-tail corruption. Offers to delete the bad cache files so MCL downloads them again - hashes every file into a manifest, robocopies, POSTs the manifest - re-sends bad small files over HTTP (no proxy), large ones by deleting the bad copy and robocopy /J (robocopy skips a same-size, same-time file as "modified" even with /IS /IT) Webapp: - services/upload_verify.py hashes the upload on the server's own disk in a background thread; reports missing, differing, bad-in-MCL-cache and stray files; PUT /api/upload/file writes temp, checks hash, renames - import of the upload folder only after a passed verification, with every file unchanged (size, mtime, ctime, inode), no stray files, and the target the OS allows (LTSC: Shop Floor only; else Standard or Engineer; unknown OS: none). Import, verify and PUT share one lock - paths matched case-insensitively against the disk, as Samba does - import page shows the upload, MCL run, verification and stray files with a remove button; record of each import kept on the image Reviewed adversarially; all 11 confirmed findings fixed with tests. 42 new unit tests. VM test: clean upload; same-size same-mtime damage on 3 files repaired in one round; stray file blocks import until removed; Shop Floor refused for a Pro upload; import records and clears the folder; missing tag, 0-byte JSON, junk-head WIM and late Control file each refused with nothing uploaded.
41 lines
1.3 KiB
Python
41 lines
1.3 KiB
Python
"""Session-based CSRF token: generate per-session, double-submit on POST."""
|
|
|
|
import secrets
|
|
|
|
from flask import abort, request, session
|
|
|
|
# Endpoint paths exempt from CSRF (machine-to-machine POSTs from PXE clients
|
|
# that have no browser session to carry a token). Air-gapped LAN; treated as
|
|
# trust-by-network. Keep the list short and explicit.
|
|
CSRF_EXEMPT_PATHS = {
|
|
"/imaging/status",
|
|
"/reports/cloud-status",
|
|
"/hardware/seen",
|
|
"/api/upload/verify", # Upload-Image.ps1 (PUT /api/upload/file is not a POST)
|
|
}
|
|
|
|
|
|
def generate_csrf_token():
|
|
"""Return the CSRF token for the current session, creating one if needed."""
|
|
if "_csrf_token" not in session:
|
|
session["_csrf_token"] = secrets.token_hex(32)
|
|
return session["_csrf_token"]
|
|
|
|
|
|
def init_csrf(app):
|
|
"""Wire CSRF protection into a Flask app: validator + template helper."""
|
|
|
|
@app.before_request
|
|
def _validate_csrf():
|
|
if request.method != "POST":
|
|
return
|
|
if request.path in CSRF_EXEMPT_PATHS:
|
|
return
|
|
token = request.form.get("_csrf_token") or request.headers.get("X-CSRF-Token")
|
|
if not token or token != generate_csrf_token():
|
|
abort(403)
|
|
|
|
@app.context_processor
|
|
def _inject_csrf_token():
|
|
return {"csrf_token": generate_csrf_token}
|