Skip to content

Commit c89736a

Browse files
committed
chore(security-headers): drop console-noise warnings from dev tools
Three real fixes (one inline-script CSP violation, six obsolete Permissions-Policy features, one helmet default that warns instead of helping). Two warnings deliberately left alone with rationale in the changelog (COOP-on-HTTP needs HTTPS in production; Edge tracking prevention on 3rd-party CDNs would need a self-hosting refactor). - src/server.js: helmet({ originAgentCluster: false }) + trim Permissions-Policy to recognized features only. - public/js/login-reset.js: extracted from inline <script> in index.html so the script-src 'self' directive accepts it. - public/index.html: <script src=…> in place of the inline block. Release: v7.3.7
1 parent de22e0f commit c89736a

8 files changed

Lines changed: 132 additions & 62 deletions

File tree

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,24 @@
22

33
All notable changes to Docker Dash are documented here.
44

5+
## [7.3.7] - 2026-04-25 — Browser console hygiene
6+
7+
A user-driven cleanup pass on the dev-tools console output. Three real issues, three drive-by warnings I left alone (and explain why).
8+
9+
### Fixed
10+
11+
- **CSP blocked inline `<script>`** in the login screen (`Executing inline script violates the following Content Security Policy directive`). The forgot-password reveal/submit handler lived inline in `public/index.html`. Extracted to [`public/js/login-reset.js`](public/js/login-reset.js) and referenced via `<script src=…>` so the existing `script-src 'self'` directive accepts it. No `unsafe-inline` was added (would defeat the point of CSP).
12+
13+
- **Permissions-Policy "Unrecognized feature" warnings** for six entries the current browsers no longer understand: `ambient-light-sensor` (early proposal, never standardized), `battery` (removed for privacy), `document-domain` (not a Permissions-Policy feature — lives in CSP/headers), `execution-while-not-rendered`, `execution-while-out-of-viewport`, `navigation-override` (all three Chrome-only, never standardized). Removed from the header in [`src/server.js`](src/server.js#L52-L74). All six are still safe at the platform level — listing them here was warning-noise, not protection.
14+
15+
- **Origin-Agent-Cluster mismatch warning** ("could not be origin-keyed since the origin had previously been placed in a site-keyed agent cluster"). Helmet defaults to sending `Origin-Agent-Cluster: ?1`, which only takes effect if every page on the origin opts in consistently — our SPA doesn't, so the warning fires on every page load. Disabled via `helmet({ originAgentCluster: false })`. We don't need agent-cluster keying for our use case.
16+
17+
### Not fixed (and why)
18+
19+
- **`Cross-Origin-Opener-Policy header has been ignored, because the URL's origin was untrustworthy`** — fires on plain HTTP. The browser refuses COOP enforcement on insecure origins. Goes away in production behind HTTPS (Caddy `--profile tls` or any other TLS termination). No code fix needed.
20+
21+
- **`Tracking Prevention blocked access to storage for <URL>`** — Edge's strict tracking prevention blocking 3rd-party storage for our CDN dependencies (jsDelivr, cdnjs, Google Fonts). Browser-side feature, can't be turned off from server. Could be eliminated by self-hosting Chart.js / FontAwesome / fonts — large refactor for a cosmetic warning. Deferred.
22+
523
## [7.3.5] - 2026-04-25 — WS cookie-first auth + What's New update banner
624

725
### Fixed

docker-compose.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ services:
44
context: .
55
dockerfile: Dockerfile
66
args:
7-
APP_VERSION: "${APP_VERSION:-7.3.5}"
8-
image: docker-dash:${APP_VERSION:-7.3.5}
7+
APP_VERSION: "${APP_VERSION:-7.3.7}"
8+
image: docker-dash:${APP_VERSION:-7.3.7}
99
container_name: docker-dash
1010
restart: unless-stopped
1111
env_file:
@@ -54,7 +54,7 @@ services:
5454
dd-egress-filter:
5555
build:
5656
context: ./docker/egress-filter
57-
image: docker-dash-egress-filter:${APP_VERSION:-7.3.5}
57+
image: docker-dash-egress-filter:${APP_VERSION:-7.3.7}
5858
container_name: dd-egress-filter
5959
restart: unless-stopped
6060
# Uses the default bridge so target containers on the default bridge can

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "docker-dash",
3-
"version": "7.3.5",
3+
"version": "7.3.7",
44
"description": "Full-featured Docker management dashboard",
55
"main": "src/server.js",
66
"scripts": {

public/index.html

Lines changed: 1 addition & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -54,58 +54,7 @@
5454
<button type="button" id="login-reset-submit" class="btn btn-primary btn-sm"><i class="fas fa-paper-plane"></i> Send reset link</button>
5555
</div>
5656
</div>
57-
<script>
58-
(function () {
59-
var link = document.getElementById('login-forgot-link');
60-
var form = document.getElementById('login-reset-form');
61-
var cancelBtn = document.getElementById('login-reset-cancel');
62-
var submitBtn = document.getElementById('login-reset-submit');
63-
var emailInput = document.getElementById('login-reset-email');
64-
var errorEl = document.getElementById('login-reset-error');
65-
var successEl = document.getElementById('login-reset-success');
66-
67-
link.addEventListener('click', function (e) {
68-
e.preventDefault();
69-
form.style.display = 'block';
70-
link.parentElement.style.display = 'none';
71-
emailInput.focus();
72-
});
73-
74-
cancelBtn.addEventListener('click', function () {
75-
form.style.display = 'none';
76-
link.parentElement.style.display = 'block';
77-
emailInput.value = '';
78-
errorEl.style.display = 'none';
79-
successEl.style.display = 'none';
80-
submitBtn.disabled = false;
81-
submitBtn.innerHTML = '<i class="fas fa-paper-plane"></i> Send reset link';
82-
});
83-
84-
submitBtn.addEventListener('click', async function () {
85-
var email = emailInput.value.trim();
86-
errorEl.style.display = 'none';
87-
successEl.style.display = 'none';
88-
if (!email) { errorEl.textContent = 'Please enter your email address.'; errorEl.style.display = 'block'; return; }
89-
submitBtn.disabled = true;
90-
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Sending...';
91-
try {
92-
await fetch('/api/auth/request-password-reset', {
93-
method: 'POST',
94-
headers: { 'Content-Type': 'application/json' },
95-
body: JSON.stringify({ email: email }),
96-
});
97-
} catch (e) { /* ignore network errors — show generic message regardless */ }
98-
successEl.textContent = "If an account exists with that email, you'll receive a reset link.";
99-
successEl.style.display = 'block';
100-
submitBtn.style.display = 'none';
101-
cancelBtn.textContent = 'Close';
102-
});
103-
104-
emailInput.addEventListener('keydown', function (e) {
105-
if (e.key === 'Enter') submitBtn.click();
106-
});
107-
})();
108-
</script>
57+
<script src="/js/login-reset.js?v=__VERSION__"></script>
10958
</form>
11059
<div id="oidc-section" class="hidden" style="margin-top:16px">
11160
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px">

public/js/login-reset.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
'use strict';
2+
3+
// Login → forgot-password reveal + submit handler. Lives on the login screen
4+
// before the main app loads, so it can't depend on Api/Toast/i18n. Pure
5+
// vanilla DOM + fetch.
6+
//
7+
// Extracted from inline <script> in index.html in v7.3.7 to comply with the
8+
// Content Security Policy (no 'unsafe-inline' for script-src).
9+
(function () {
10+
function init() {
11+
var link = document.getElementById('login-forgot-link');
12+
var form = document.getElementById('login-reset-form');
13+
var cancelBtn = document.getElementById('login-reset-cancel');
14+
var submitBtn = document.getElementById('login-reset-submit');
15+
var emailInput = document.getElementById('login-reset-email');
16+
var errorEl = document.getElementById('login-reset-error');
17+
var successEl = document.getElementById('login-reset-success');
18+
19+
if (!link || !form || !cancelBtn || !submitBtn || !emailInput || !errorEl || !successEl) {
20+
return; // Login markup not present (rare in non-app contexts) — bail
21+
}
22+
23+
link.addEventListener('click', function (e) {
24+
e.preventDefault();
25+
form.style.display = 'block';
26+
link.parentElement.style.display = 'none';
27+
emailInput.focus();
28+
});
29+
30+
cancelBtn.addEventListener('click', function () {
31+
form.style.display = 'none';
32+
link.parentElement.style.display = 'block';
33+
emailInput.value = '';
34+
errorEl.style.display = 'none';
35+
successEl.style.display = 'none';
36+
submitBtn.disabled = false;
37+
submitBtn.innerHTML = '<i class="fas fa-paper-plane"></i> Send reset link';
38+
});
39+
40+
submitBtn.addEventListener('click', async function () {
41+
var email = emailInput.value.trim();
42+
errorEl.style.display = 'none';
43+
successEl.style.display = 'none';
44+
if (!email) {
45+
errorEl.textContent = 'Please enter your email address.';
46+
errorEl.style.display = 'block';
47+
return;
48+
}
49+
submitBtn.disabled = true;
50+
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Sending...';
51+
try {
52+
await fetch('/api/auth/request-password-reset', {
53+
method: 'POST',
54+
headers: { 'Content-Type': 'application/json' },
55+
body: JSON.stringify({ email: email }),
56+
});
57+
} catch (e) { /* ignore network errors — show generic message regardless */ }
58+
successEl.textContent = "If an account exists with that email, you'll receive a reset link.";
59+
successEl.style.display = 'block';
60+
submitBtn.style.display = 'none';
61+
cancelBtn.textContent = 'Close';
62+
});
63+
64+
emailInput.addEventListener('keydown', function (e) {
65+
if (e.key === 'Enter') submitBtn.click();
66+
});
67+
}
68+
69+
// The script tag is loaded after the markup, so DOM is already parsed,
70+
// but tolerate both timings.
71+
if (document.readyState === 'loading') {
72+
document.addEventListener('DOMContentLoaded', init);
73+
} else {
74+
init();
75+
}
76+
})();

public/js/pages/whatsnew.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@ const WhatsNewPage = {
99
// Add new releases at the TOP of this array.
1010
// Types: feature, fix, improvement, security, breaking
1111
_releases: [
12+
{
13+
version: '7.3.7',
14+
date: '2026-04-25',
15+
title: 'Browser console hygiene',
16+
changes: [
17+
{ type: 'fix', text: 'CSP blocked an inline <script> on the login screen (the forgot-password reveal/submit handler). Extracted to public/js/login-reset.js so the existing script-src \'self\' directive accepts it. No \'unsafe-inline\' added.' },
18+
{ type: 'fix', text: 'Six "Unrecognized feature" warnings in the Permissions-Policy header — removed entries that current browsers no longer understand: ambient-light-sensor, battery, document-domain, execution-while-not-rendered, execution-while-out-of-viewport, navigation-override. All six are still safe at the platform level — listing them here was warning-noise, not protection.' },
19+
{ type: 'fix', text: 'Origin-Agent-Cluster mismatch warning ("could not be origin-keyed"). Helmet sends ?1 by default but our SPA doesn\'t opt every page in consistently. Disabled via helmet({ originAgentCluster: false }) since we don\'t need agent-cluster keying.' },
20+
{ type: 'improvement', text: 'Two warnings left intentionally: COOP-on-HTTP (browser refuses on insecure origin — goes away behind HTTPS) and Edge Tracking Prevention blocking 3rd-party CDN storage (browser feature, would need self-hosting Chart.js + FontAwesome + fonts to silence — deferred).' },
21+
],
22+
},
1223
{
1324
version: '7.3.5',
1425
date: '2026-04-25',

src/server.js

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,18 +46,34 @@ app.use(helmet({
4646
},
4747
},
4848
frameguard: { action: 'deny' },
49+
// v7.3.7: disable Origin-Agent-Cluster (Helmet default sends `?1`).
50+
// Without explicitly opting in on every page, the header gets ignored
51+
// with a console warning ("could not be origin-keyed since the origin
52+
// had previously been placed in a site-keyed agent cluster"). We don't
53+
// need agent-cluster keying for our SPA, so just stop sending it.
54+
originAgentCluster: false,
4955
}));
5056

5157
// Permissions-Policy — explicitly deny browser APIs we never use. Any future
5258
// feature that needs one of these (e.g. audio notifications) must opt-in here.
59+
// v7.3.7: dropped 6 features that current browsers don't recognize (Edge
60+
// console flagged each as "Unrecognized feature"):
61+
// - ambient-light-sensor (early proposal, never standardized)
62+
// - battery (removed from spec for privacy)
63+
// - document-domain (not a Permissions-Policy feature; lives in CSP/headers)
64+
// - execution-while-not-rendered, execution-while-out-of-viewport (Chrome-only,
65+
// never standardized)
66+
// - navigation-override (Chrome-only, never standardized)
67+
// All six are still safe defaults at the platform level — listing them
68+
// here was warning-noise, not protection.
5369
app.use((req, res, next) => {
5470
res.setHeader(
5571
'Permissions-Policy',
56-
'accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), ' +
57-
'cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), ' +
58-
'execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(self), ' +
59-
'geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), ' +
60-
'midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), ' +
72+
'accelerometer=(), autoplay=(), camera=(), ' +
73+
'cross-origin-isolated=(), display-capture=(), encrypted-media=(), ' +
74+
'fullscreen=(self), geolocation=(), gyroscope=(), keyboard-map=(), ' +
75+
'magnetometer=(), microphone=(), midi=(), payment=(), ' +
76+
'picture-in-picture=(), publickey-credentials-get=(), ' +
6177
'screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()'
6278
);
6379
next();

src/version.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
// Single source of truth for the application version.
33
// Updated automatically by: npm version X.Y.Z (via scripts/sync-version.js)
44
// server.js reads this to inject into index.html at startup — no build step needed.
5-
module.exports = '7.3.5';
5+
module.exports = '7.3.7';

0 commit comments

Comments
 (0)