You probably need to extend dispatcher.uc
to pass the generated nonce value into the template engine, then modify the various places where inline scripts are present.
Untested patch implementing the functionality here:
diff --git a/modules/luci-base/ucode/dispatcher.uc b/modules/luci-base/ucode/dispatcher.uc
index 8717385be2..c1f9862d61 100644
--- a/modules/luci-base/ucode/dispatcher.uc
+++ b/modules/luci-base/ucode/dispatcher.uc
@@ -865,6 +865,7 @@ dispatch = function(_http, path) {
let version = determine_version();
let lang = determine_request_language();
+ let nonce = b64enc(randomid(16));
runtime = runtime || LuCIRuntime({
http,
@@ -886,7 +887,8 @@ dispatch = function(_http, path) {
randomid,
error404,
error500,
- lang
+ lang,
+ nonce
},
striptags,
entityencode,
@@ -925,6 +927,7 @@ dispatch = function(_http, path) {
http.status(403, 'Forbidden');
http.header('X-LuCI-Login-Required', 'yes');
+ http.header('Content-Security-Policy', `script-src 'nonce-${nonce}' 'unsafe-eval'`);
let scope = { duser: 'root', fuser: user };
let theme_sysauth = `themes/${basename(runtime.env.media)}/sysauth`;
@@ -1000,6 +1003,8 @@ dispatch = function(_http, path) {
if (require_post_security(action) && !test_post_security(resolved.ctx.authtoken))
return;
+ http.header('Content-Security-Policy', `script-src 'nonce-${nonce}' 'unsafe-eval'`);
+
run_action(path, lang, menu, resolved, action);
}
catch (ex) {
diff --git a/modules/luci-base/ucode/template/footer.ut b/modules/luci-base/ucode/template/footer.ut
index d0978594f8..e11b28a244 100644
--- a/modules/luci-base/ucode/template/footer.ut
+++ b/modules/luci-base/ucode/template/footer.ut
@@ -5,7 +5,7 @@
{% const rollback = dispatcher.rollback_pending() %}
{% if (rollback || trigger_apply || trigger_revert): %}
- <script type="text/javascript">
+ <script type="text/javascript" nonce="{{ dispatcher.nonce }}">
document.addEventListener("luci-loaded", function() {
{% if (trigger_apply): %}
L.ui.changes.apply(true);
@@ -19,7 +19,7 @@
{% endif %}
{% if (media_error): %}
- <script type="text/javascript">
+ <script type="text/javascript" nonce="{{ dispatcher.nonce }}">
L.require('ui').then(function(ui) {
ui.showIndicator('media_error', _('Theme fallback'), function(ev) {
ui.showModal(_('Error loading theme'), [
diff --git a/modules/luci-base/ucode/template/header.ut b/modules/luci-base/ucode/template/header.ut
index 7dc3742a9d..954116f929 100644
--- a/modules/luci-base/ucode/template/header.ut
+++ b/modules/luci-base/ucode/template/header.ut
@@ -7,9 +7,9 @@
include(`themes/${theme}/header`);
-%}
-<script type="text/javascript" src="{{ resource }}/promis.min.js"></script>
-<script type="text/javascript" src="{{ resource }}/luci.js"></script>
-<script type="text/javascript">
+<script type="text/javascript" src="{{ resource }}/promis.min.js" nonce="{{ dispatcher.nonce }}"></script>
+<script type="text/javascript" src="{{ resource }}/luci.js" nonce="{{ dispatcher.nonce }}"></script>
+<script type="text/javascript" nonce="{{ dispatcher.nonce }}">
L = new LuCI({{ replace(`${ {
media : media,
resource : resource,
diff --git a/modules/luci-base/ucode/template/view.ut b/modules/luci-base/ucode/template/view.ut
index 11ac824290..d729c10f15 100644
--- a/modules/luci-base/ucode/template/view.ut
+++ b/modules/luci-base/ucode/template/view.ut
@@ -2,7 +2,7 @@
<div id="view">
<div class="spinning">{{ _('Loading view…') }}</div>
- <script type="text/javascript">
+ <script type="text/javascript" nonce="{{ dispatcher.nonce }}">
L.require('ui').then(function(ui) {
ui.instantiateView('{{ view }}');
});
diff --git a/themes/luci-theme-bootstrap/ucode/template/themes/bootstrap/footer.ut b/themes/luci-theme-bootstrap/ucode/template/themes/bootstrap/footer.ut
index 6031724053..33ce7178b2 100644
--- a/themes/luci-theme-bootstrap/ucode/template/themes/bootstrap/footer.ut
+++ b/themes/luci-theme-bootstrap/ucode/template/themes/bootstrap/footer.ut
@@ -14,7 +14,7 @@
</span>
<ul class="breadcrumb pull-right" id="modemenu" style="display:none"></ul>
</footer>
- <script type="text/javascript">L.require('menu-bootstrap')</script>
+ <script type="text/javascript" nonce="{{ dispatcher.nonce }}">L.require('menu-bootstrap')</script>
{% endif %}
</body>
</html>
diff --git a/themes/luci-theme-bootstrap/ucode/template/themes/bootstrap/header.ut b/themes/luci-theme-bootstrap/ucode/template/themes/bootstrap/header.ut
index b7bc770b4b..e9ac1ca713 100644
--- a/themes/luci-theme-bootstrap/ucode/template/themes/bootstrap/header.ut
+++ b/themes/luci-theme-bootstrap/ucode/template/themes/bootstrap/header.ut
@@ -20,7 +20,7 @@
<meta charset="utf-8">
<title>{{ striptags(`${boardinfo.hostname ?? '?'}${node ? ` - ${node.title}` : ''}`) }} - LuCI</title>
{% if (!darkpref): %}
- <script type="text/javascript">
+ <script type="text/javascript" nonce="{{ dispatcher.nonce }}">
var mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'),
rootElement = document.querySelector(':root'),
setDarkMode = function(match) { rootElement.setAttribute('data-darkmode', match.matches) };
@@ -39,8 +39,8 @@
{% if (css): %}
<style title="text/css">{{ css }}</style>
{% endif %}
- <script src="{{ dispatcher.build_url('admin/translations', dispatcher.lang) }}"></script>
- <script src="{{ resource }}/cbi.js"></script>
+ <script src="{{ dispatcher.build_url('admin/translations', dispatcher.lang) }}" nonce="{{ dispatcher.nonce }}"></script>
+ <script src="{{ resource }}/cbi.js" nonce="{{ dispatcher.nonce }}"></script>
</head>
<body class="lang_{{ dispatcher.lang }} {{ entityencode(striptags(node?.title ?? ''), true) }}" data-page="{{ entityencode(join('-', ctx.request_path), true) }}">
diff --git a/themes/luci-theme-bootstrap/ucode/template/themes/bootstrap/sysauth.ut b/themes/luci-theme-bootstrap/ucode/template/themes/bootstrap/sysauth.ut
index 15f3b1435b..fc79f3ffa5 100644
--- a/themes/luci-theme-bootstrap/ucode/template/themes/bootstrap/sysauth.ut
+++ b/themes/luci-theme-bootstrap/ucode/template/themes/bootstrap/sysauth.ut
@@ -38,7 +38,7 @@
<div id="view">
<div class="spinning">{{ _('Loading view…') }}</div>
- <script type="text/javascript">
+ <script type="text/javascript" nonce="{{ dispatcher.nonce }}">
L.require('ui').then(function(ui) {
ui.instantiateView('bootstrap.sysauth');
});
Older, Lua-based LuCI modifications would look similar, you would need to extend dispatcher.lua
in that case.