The inline-script check matched <script\b[^>]*> and then filtered the matched text, so a > inside an attribute value split one tag into a fragment that had already lost the attribute the filter looks for. An html.parser subclass decides on the parsed attributes instead. The i18n test imported unittest twice, once plain and once as a from-import.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
base.html.j2 set window.I18N from an executable inline <script>. A host
CSP can only allow an inline script by hash or by 'unsafe-inline', and this
script's content changes with every language, so no hash can cover it.
Infinito.Nexus serves the dashboard with a hash-based script-src-elem
whenever its logout feature is off, and there the script was blocked:
every page logged "Executing inline script violates the following Content
Security Policy directive 'script-src-elem ...'" and window.I18N stayed
undefined.
The strings now ship as <script id="i18n" type="application/json">, which
the browser does not execute and CSP does not govern; modal.js parses the
block before its first use. tojson escapes <, > and &, so a string that
contains "</script>" cannot end the block early.
Integration tests require that a page ships no executable inline script
and that a catalogue string containing "</script>" survives the round trip
through the block.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ProxyFix defaults x_for to 1, so ProxyFix(app.wsgi_app, x_proto=1) never
disabled it: request.remote_addr and the access log were forgeable by any
client that reached the app directly. It is x_for=0 now, asserted rather
than assumed.
A mutation audit over the change set reverted 196 deliberate behaviours
and found 47 that no test noticed. This closes the ones that carry damage:
- apod_background lost its key check, its transport guard, its status
guard and its media-type check without a single test failing. Each one
turns a slow or unhappy NASA into a 500 on every page.
- Untrusted values reached innerHTML through window.I18N, which the
translation backend writes, and the modal's click handlers stacked so a
later click opened an earlier popup's URL.
- The sync tool could ask for HTML instead of text, translate from "auto"
instead of English, run without a timeout, store an empty translation
that marks the string done for good, abandon 28 languages because one
could not be written, and report success after reaching nothing.
- Neither the lint target, the CI jobs, the vendored RTL stylesheet, the
documented environment keys, nor any of the four hardenings in
scripts/run-e2e.sh was observed by anything.
Three of the new tests passed for the wrong reason on their first cut —
a mock that answered None whether or not the guard existed, a
raise_for_status that was never called, a string that stayed in the file
after the mutation. The audit found those too; all 24 reverts now fail.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The interface ships translated; page content stays English until a
LibreTranslate instance fills app/i18n/content/ through make i18n. A string
without a catalogue entry falls back to its English source, so a half-filled
catalogue degrades instead of breaking.
Translation runs after ConfigurationResolver.resolve_links(), on a copy.
resolve_links matches by the `name` field, so translating it beforehand
would break every `link:` reference in the configuration.
negotiate() normalises to the primary subtag itself. Werkzeug's best_match
returns an exact match before it considers a primary-tag fallback, so the
Chrome default `de-DE,en;q=0.8` resolves to English there. "/" carries
Vary: Accept-Language, without which a shared cache pins the first
visitor's language for everyone.
The route rule lists the known codes as a converter argument. A bare
"/<lang>/" answers /robots.txt and /favicon.ico with a permanently
cacheable 308 to their trailing-slash form.
Templates gain lang, dir, the RTL stylesheet, a canonical URL and 30
hreflang alternates. Those are the first external URLs in this app:
ProxyFix takes the scheme from X-Forwarded-Proto so they do not claim
http:// behind a TLS-terminating proxy, X-Forwarded-Host stays untrusted
because nginx passes a client-supplied one through, and TRUSTED_HOSTS lets
Flask reject a forged Host outright.
Flask only autoescapes .html/.htm/.xml/.xhtml/.svg, so every *.html.j2
template interpolated configuration raw. Enabling it changes two lines of
the shipped page, both an apostrophe.
read_catalog degrades an unreadable catalogue to English rather than
serving a 500, and drops non-string entries that would otherwise render as
"42". i18n_sync writes atomically, never overwrites an existing entry,
refuses to touch a catalogue it could not parse, and leaves the file alone
when a run translated nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>