feat(offline): la policy arriva dove le pagine si aprono davvero
Punto 12. Le cinque librerie erano già state portate in casa (28ee44b): nessun
template carica più niente dalla rete, il worker di PDF.js è locale in tutti e
quattro i file che lo impostano, i font sono woff2 nel pacchetto. Verificato riga
per riga, e le impronte SHA-256 in VERSIONS.md corrispondono ancora.
Mancava però la seconda metà dell'intervento, e mancava dove conta. La
Content-Security-Policy a sola origine locale esisteva sul backend, cioè sulle
risposte API; le pagine HTML le serve il client Flask, che non mandava alcuna
policy. La regola stava scritta dove non poteva essere infranta e assente dove
poteva. Ora il client la manda su ogni risposta.
Serve meno a difendere e più a non far tornare indietro il punto: un tag verso un
CDN aggiunto fra sei mesi viene rifiutato dal browser alla scrivania, dove c'è la
rete e l'errore si legge in console, invece che in reparto dove la rete non c'è.
Tailwind era ancora agganciato a `tailwindcss@3` nel Dockerfile: stesso difetto
che il documento cita per le librerie del browser, un gradino più in basso. Fissato
a 3.4.19, che è la versione con cui l'immagine in esercizio è stata costruita.
I test non renderizzano niente: leggono i sorgenti, perché il difetto che devono
impedire si scrive in un template e non si vede finché non si stacca la rete.
Controllano anche le impronte — una libreria sostituita sul posto tiene lo stesso
nome e la stessa riga in tabella, e l'hash è l'unica parte che se ne accorge.
Annotato in VERSIONS.md che `html5-qrcode` è l'unica libreria dichiarata e mai
caricata: lo scanner da fotocamera non è incluso da nessuna pagina, il lettore che
l'operatore usa è un campo di testo. Va portata in casa prima di accenderlo.
README aggiornato a V3.0.0: novità della versione punto per punto, ruolo
Supervisor, librerie locali al posto dei CDN, stato dei test.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,28 @@ from config import Config
|
||||
# line break. DOTALL so a phrase that wraps onto the next line still closes.
|
||||
_BOLD_RX = re.compile(r"\*\*(.+?)\*\*", re.DOTALL)
|
||||
|
||||
# Punto 12. Nothing loads from outside: every library ships with the install
|
||||
# (static/vendor/), because the shop floor network has no way out and a page that
|
||||
# waits for a CDN there is a white screen, not a slow one.
|
||||
#
|
||||
# unsafe-inline the templates carry their Alpine components inline
|
||||
# unsafe-eval Alpine 3 and Plotly both compile expressions at runtime
|
||||
# blob: PDF.js runs its worker from a blob, Fabric exports canvases
|
||||
# data: thumbnails and canvas exports are inlined
|
||||
CSP = (
|
||||
"default-src 'self'; "
|
||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; "
|
||||
"style-src 'self' 'unsafe-inline'; "
|
||||
"font-src 'self'; "
|
||||
"img-src 'self' data: blob:; "
|
||||
"connect-src 'self'; "
|
||||
"worker-src 'self' blob:; "
|
||||
"object-src 'none'; "
|
||||
"base-uri 'self'; "
|
||||
"form-action 'self'; "
|
||||
"frame-ancestors 'none'"
|
||||
)
|
||||
|
||||
|
||||
def get_locale():
|
||||
"""Get user's preferred language from session or Accept-Language header."""
|
||||
@@ -115,6 +137,25 @@ def create_app() -> Flask:
|
||||
)
|
||||
return Markup(rv)
|
||||
|
||||
@app.after_request
|
||||
def security_headers(response):
|
||||
"""Same-origin only, on the pages the browser actually loads.
|
||||
|
||||
The policy existed on the backend, which serves the API; the HTML comes
|
||||
from here and carried no policy at all. So the rule that says «nothing
|
||||
from outside» was written where it could not be broken and absent where
|
||||
it could.
|
||||
|
||||
It is also the guard that makes point 12 stay done: a CDN tag added to a
|
||||
template months from now is refused by the browser here, at a desk with a
|
||||
network, instead of on a shop floor that has none.
|
||||
"""
|
||||
response.headers.setdefault("Content-Security-Policy", CSP)
|
||||
response.headers.setdefault("X-Content-Type-Options", "nosniff")
|
||||
response.headers.setdefault("X-Frame-Options", "DENY")
|
||||
response.headers.setdefault("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||
return response
|
||||
|
||||
@app.context_processor
|
||||
def inject_globals():
|
||||
"""Inject global variables into all templates."""
|
||||
|
||||
Reference in New Issue
Block a user