luci-base: add nonce-based CSP support to replace unsafe-inline#8788
luci-base: add nonce-based CSP support to replace unsafe-inline#8788maxskokov wants to merge 5 commits into
Conversation
The CSP plugin previously used 'unsafe-inline' in script-src because
there was no mechanism to pass a per-request nonce to http header
plugins. This commit adds that mechanism across the stack:
- dispatcher.uc: generate a per-request nonce via randomid(16) and
store it in both ctx.csp_nonce (for templates) and http.csp_nonce
(for http header plugins)
- luciplugins.uc: extend run_plugins() with an optional extra_ctx arg,
passed to each plugin — backward compatible, existing plugins ignore
extra args
- http.uc: forward { csp_nonce } to run_plugins() from write_headers()
- core .ut templates and all themes: add nonce attribute to inline
<script> tags using ctx?.csp_nonce
- luci-plugin-csp: use 'nonce-' instead of 'unsafe-inline'
when a nonce is available, falling back to 'unsafe-inline' otherwise
Signed-off-by: Maxim Skokov <skokov.m020709@gmail.com>
openwrt-ai
left a comment
There was a problem hiding this comment.
One inline note on the CSP script-src change. The <script>-tag nonce plumbing itself is coherent (nonce is generated in dispatch() before templates render and before write_headers() emits the header, and the header's 'nonce-<hex>' matches the nonce="<hex>" attribute value), and the single commit's message accurately describes the diff.
Generated by Claude Code
| const uci = cursor(); | ||
| const mode = uci.get('luci_plugins', plugin_id, 'mode') || 'strict'; | ||
|
|
||
| const nonce = extra_ctx?.csp_nonce ? `'nonce-${extra_ctx.csp_nonce}'` : "'unsafe-inline'"; |
There was a problem hiding this comment.
Once a nonce is present in script-src, CSP2+ browsers ignore 'unsafe-inline' in that directive — and here 'unsafe-inline' is dropped from script-src entirely when a nonce is available. Nonces only whitelist <script> elements; they do not cover inline event-handler attributes (onclick=, onchange=, onsubmit=, onkeyup=, …) or javascript: URIs, which previously worked solely because of 'unsafe-inline'. Those will now be blocked whenever the plugin runs in strict or permissive mode.
Inline handlers are still used in-tree, e.g. core csrftoken.ut:21 (onclick="window.history.back();"), extensively across the luci-compat CBI templates (footer.htm, tblsection.htm, tsection.htm, simpleform.htm, header.htm, …), and in luci-app-commands/commands.ut:151. Their buttons/links would silently stop responding under the hardened policy.
The <script>-tag migration in this PR doesn't reach these attributes. Consider either migrating the remaining inline handlers to addEventListener in nonced scripts, or carrying 'unsafe-hashes' plus the corresponding hashes in the policy for handlers that must stay inline. At minimum this behavior change is worth calling out in the commit/PR description.
Generated by Claude Code
Inline event handlers (onclick, onsubmit, onreset, onkeyup, onchange)
are blocked by CSP nonce policies because nonces only cover <script>
elements, not event attributes or javascript: URIs.
Extend cbi_init() to wire up the following patterns declaratively so
that templates can remove all inline handlers:
[data-href] -> location.href navigation
[data-cbi-submit] -> cbi_submit() for named form actions
[data-cbi-state] -> sets form.cbi_state before submit
[data-cbi-rowswap] -> cbi_row_swap() with data-cbi-sortstore
.cbi-section-create-name -> keyup cbi_validate_named_section_add()
.cbi-cidr-toggle -> switchToCIDRList() click handler
form[name="cbi"] -> submit/reset validation via cbi_validate_form()
and cbi_validate_reset(), error message read
from data-error-msg attribute
Also move switchToCIDRList() from ipaddr.htm into cbi.js where it
belongs alongside the other CBI helpers.
Remove the remaining onclick from csrftoken.ut by adding a nonce'd
<script> block with an addEventListener call.
Signed-off-by: Maxim Skokov <skokov.m020709@gmail.com>
Add csp_nonce as a named key in the Lua template viewns, resolved from disp.context.csp_nonce (which maps to the ucode-side ctx.csp_nonce set by the dispatcher). This lets legacy Lua/CBI templates reference the per-request nonce as a plain variable, consistent with how token and REQUEST_URI are already exposed. Required for adding nonce="<%=csp_nonce%>" to inline <script> blocks in luci-compat CBI templates. Signed-off-by: Maxim Skokov <skokov.m020709@gmail.com>
CSP nonce policies block inline event attributes (onclick, onsubmit,
onreset, onkeyup, onchange) even when <script> elements are nonce-gated.
Convert all such handlers in the CBI Lua templates to data-* attributes
that cbi_init() now handles declaratively:
footer.htm: onclick on Back/Skip/Apply/Reset -> data-href,
data-cbi-submit; nonce on <script>cbi_init()
header.htm: remove onsubmit/onreset from form, add data-error-msg
so cbi_init() can wire up cbi_validate_form()
simpleform.htm: onclick on Back/Cancel/Skip -> data-href, data-cbi-submit;
nonce on <script>cbi_init()
tblsection.htm: onclick on Up/Down/Edit/Delete/Add -> data-cbi-rowswap,
data-href, data-cbi-state; onkeyup on name input removed
(cbi_init() now binds .cbi-section-create-name)
tsection.htm: same pattern for Delete/Add
ipaddr.htm: remove inline <script> (switchToCIDRList moved to cbi.js),
add class cbi-cidr-toggle to the CIDR toggle button
wireless_modefreq.htm: add nonce to both <script> blocks; remove onchange
from mode/band selects; wire them up in the post-init
nonce'd script that already calls cbi_init_wifi()
Signed-off-by: Maxim Skokov <skokov.m020709@gmail.com>
The inline <script> block lacked a nonce attribute, and the Run/Download/
Link buttons used onclick handlers — both blocked by CSP nonce policy.
Add nonce="{{ ctx?.csp_nonce }}" to the <script> block. Replace the
three onclick attributes with data-action and data-cmd attributes, then
register a single DOMContentLoaded listener inside the nonce'd script
that dispatches to command_run(), command_download() or command_link()
based on data-action.
Signed-off-by: Maxim Skokov <skokov.m020709@gmail.com>
|
Migration scope note: this PR switches the CSP The rest of the tree isn't migrated yet, so enabling
Flagging so we can decide whether to widen this PR's scope before merge, or split the rest off into follow-up PRs (same pattern as #8762 splitting off from #8749). |
Pull request details
Description
The CSP plugin (
luci-plugin-csp) used'unsafe-inline'inscript-srcbecause there was no mechanism to pass a per-request nonce to http header
plugins. This adds that mechanism across the stack with minimal changes.
dispatcher.uc: generate a per-request nonce viarandomid(16)and storeit in
ctx.csp_nonce(available in templates viaruntime.env) andhttp.csp_nonce(available inwrite_headers()).luciplugins.uc: extendrun_plugins()with an optionalextra_ctxargforwarded to each plugin — backward compatible, existing plugins ignore it.
http.uc: pass{ csp_nonce: this.csp_nonce }intorun_plugins()fromwrite_headers().Core
.uttemplates and all themes: addnonce="{{ ctx?.csp_nonce }}"toinline
<script>tags.luci-plugin-csp: use'nonce-${nonce}'instead of'unsafe-inline'whena nonce is available, falling back to
'unsafe-inline'for backwardscompatibility.
Follows up on the discussion in #8785.
Follow-up: inline event handler migration
Addressed the reviewer note that CSP2+ browsers ignore
'unsafe-inline'inscript-srconce a nonce is present, meaning inline event attributes(
onclick,onsubmit,onreset,onkeyup,onchange) andjavascript:URIs are blocked even when
<script>elements are nonce-gated.luci-base/cbi.js: extendedcbi_init()to wire up all affectedhandler patterns declaratively via
data-*attributes:[data-href],[data-cbi-submit],[data-cbi-state],[data-cbi-rowswap],.cbi-section-create-name(keyup),.cbi-cidr-toggle(click), andform[name="cbi"]submit/reset validation (error message read fromdata-error-msg). Also movedswitchToCIDRList()fromipaddr.htmintocbi.js.luci-lua-runtime/template.lua: exposedcsp_nonceas a named keyin the Lua template viewns (same pattern as
tokenandREQUEST_URI),making it accessible as a plain variable in legacy CBI
.htmtemplates.luci-compat: removed all inline event attributes fromheader.htm,footer.htm,simpleform.htm,tblsection.htm,tsection.htm,ipaddr.htm, andwireless_modefreq.htm. Replaced withdata-*attributeshandled by
cbi_init(). Addednonce="<%=csp_nonce%>"to all inline<script>blocks.luci-app-commands: addednonce="{{ ctx?.csp_nonce }}"to the inline<script>block; replacedonclickon Run/Download/Link buttons withdata-action/data-cmdattributes dispatched via aDOMContentLoadedlistener inside the nonce'd script.
Maintainer (preferred)
@jow-
Tested on
Verified by code inspection.
Checklist
Signed-off-by: <my@email.address>row (viagit commit --signoff).<package name>: titlefirst line subject for packages.