Conversation
There was a problem hiding this comment.
Pull request overview
This PR hardens ODIN’s browser-side configuration import/export surfaces (Designer + Sizer) by treating imported/shared state as untrusted input, adding bounds, and introducing security-focused regressions, while also publishing the changes as version 0.22.71.
Changes:
- Adds XSS and prototype-pollution defenses for imported/shared configuration (escaping rendered values, limiting accepted keys, normalizing IDs).
- Adds import/export guardrails (5 MB file limits, URL/config bounds, workload/name caps) and CSV spreadsheet formula neutralization.
- Updates dependency classification + CI install behavior (
html-validatedev-only; CI usesnpm ci) and adds security regression tests.
Reviewed changes
Copilot reviewed 11 out of 13 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
tests/index.html |
Adds regression coverage for Designer/Sizer import hardening and CSV export neutralization. |
sizer/sizer.js |
Escapes imported workload rendering, normalizes handler IDs, adds bounds, and introduces CSV cell escaping. |
sizer/index.html |
Bumps visible Sizer version to 0.22.71. |
README.md |
Updates “Latest Release” notes to 0.22.71 with security-hardening bullets. |
package.json |
Moves html-validate to devDependencies to match “static site + tooling” architecture. |
package-lock.json |
Reflects html-validate (and transitive tree) as dev-only. |
js/script.js |
Restricts Designer imports to known keys, adds file size bound, and escapes printable-document output. |
js/changelog.js |
Adds 0.22.71 entry in the in-app changelog. |
js/analytics.js |
Updates documented Firebase rule guidance for increment-only counters (comment example). |
index.html |
Bumps visible Designer version to 0.22.71. |
docs/module-planning/json-import-security-hardening-plan.md |
Documents the import-hardening plan and validation steps. |
CHANGELOG.md |
Adds 0.22.71 release notes describing security hardening and dependency/CI changes. |
.github/workflows/test.yml |
Switches HTML validation job install step to npm ci for reproducible installs. |
Files not reviewed (1)
- js/changelog.js: Generated file
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 13 changed files in this pull request and generated 1 comment.
Files not reviewed (1)
- js/changelog.js: Generated file
Comments suppressed due to low confidence (1)
docs/module-planning/json-import-security-hardening-plan.md:3
- This planning note is marked as "implemented and validated". Per the repo’s planning-notes convention, completed plans should be moved under
docs/module-planning/complete/to keep the top-level folder focused on in-flight work.
# JSON Import Security Hardening Plan
Status: **implemented and validated** for version 0.22.71.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 13 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- js/changelog.js: Generated file
Comments suppressed due to low confidence (3)
sizer/sizer.js:5138
renderWorkloads()interpolatesw.typedirectly into aclass="..."attribute. Becauseworkloadscan be restored from untrusted localStorage (seeresumeSizerState()), a tamperedw.typecontaining quotes can break out of the attribute and inject new attributes/handlers. Also,actionIdcurrently accepts negative safe integers (e.g. "-5") instead of falling back to-1. Restrict the CSS class and handler IDs to known-safe values.
}
updateHwGpuTypeLock();
return;
}
sizer/sizer.js:5151
getWorkloadDetails()returns an HTML anchor for GHEL workloads (the “(sizing info)” link). Escaping the entiredetailsstring inrenderWorkloads()causes that link markup to render as literal text and no longer be clickable. Consider restructuring so only user-controlled values are escaped while trusted link markup is appended separately.
${getWorkloadIcon(workloadType)}
</div>
<div class="workload-card-content">
docs/module-planning/json-import-security-hardening-plan.md:3
- This plan is marked as "implemented and validated" for v0.22.71, which implies the work is shipping in this release. The module-planning folder is intended for in-flight plans; shipped plans should be moved under
docs/module-planning/complete/to keep the top level focused on active work (seedocs/module-planning/README.md).
# JSON Import Security Hardening Plan
Status: **implemented and validated** for version 0.22.71.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 13 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- js/changelog.js: Generated file
Comments suppressed due to low confidence (4)
tests/index.html:299
- This test exercises the new import hardening via applyKnownDesignerState(), but the real Designer import flow goes through prepareDesignerImport() + application in importConfiguration(). Using a test-only helper here can give false confidence (it may keep passing even if the production import starts accepting unknown/dangerous keys again). Update the regression to call prepareDesignerImport and assert skippedKeys + no live-state mutation.
(function() {
const originalScenario = state.scenario;
const imported = JSON.parse('{"scenario":"connected","__proto__":{"polluted":true},"unexpectedHtml":"<img src=x onerror=alert(1)>"}');
const skipped = applyKnownDesignerState(imported);
const safe = state.scenario === 'connected' &&
sizer/sizer.js:5153
- Using
w.name || ''treats the numeric value0as empty, which can silently drop an imported or programmatic workload name. Coerce to string while preserving0/falsey-but-valid values before escaping.
${escapeHtmlSizer(w.name || '')}
sizer/sizer.js:9874
- isDesignerExportPayload() currently treats any JSON object with a
stateobject as a Designer export. This can misclassify unrelated JSON (or future Sizer payloads that happen to includestate) and block valid imports with a misleading message. Make the detection more specific to the actual Designer export envelope (e.g., require a stringversionplusstate).
function isDesignerExportPayload(parsed) {
return Boolean(parsed && typeof parsed === 'object' && !Array.isArray(parsed) &&
parsed.state && typeof parsed.state === 'object' && !Array.isArray(parsed.state));
}
docs/module-planning/json-import-security-hardening-plan.md:3
- This plan is marked "implemented and validated"; per the repository convention for shipped plans (see docs/module-planning/complete/README.md), it should be moved under docs/module-planning/complete/ so the top-level module-planning folder stays focused on in-flight work.
Status: **implemented and validated** for version 0.22.71.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 18 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- js/changelog.js: Generated file
Comments suppressed due to low confidence (5)
js/notifications.js:80
- openInteractionDialog() relies on .showModal(), which is not available in Safari 14 (README lists Safari 14+ as supported). Without a feature check, calling showModal will throw and break Reset/Share/Export dialogs on supported browsers; add a native prompt/confirm fallback when showModal is unavailable.
function openInteractionDialog(options) {
const settings = options || {};
const previousFocus = document.activeElement;
const dialog = document.createElement('dialog');
const form = document.createElement('form');
js/notifications.js:140
- openInteractionDialog(): input.value is assigned using
settings.input.value || '', which drops valid falsy values like 0. Use a null/undefined check so numeric values (or the string "0") are preserved when provided by callers.
input = document.createElement(settings.input.multiline ? 'textarea' : 'input');
input.className = settings.input.readOnly ? 'odin-dialog__copy-value' : 'odin-dialog__input';
input.value = settings.input.value || '';
input.readOnly = Boolean(settings.input.readOnly);
if (!settings.input.multiline) input.type = settings.input.type || 'text';
css/interactions.css:152
- The focus-ring style uses
color-mix(...), which is not supported in Safari 14 (listed as supported in README). In unsupported browsers the box-shadow will be dropped, reducing visible focus indication; prefer a compatible focus ring using existing CSS variables.
.odin-dialog__input:focus,
.odin-dialog__copy-value:focus {
border-color: var(--accent-blue);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-blue) 30%, transparent);
}
docs/module-planning/shared-interactions-plan.md:4
- This planning note is marked "implemented and validated". Per docs/module-planning/README.md, shipped plans should be moved under docs/module-planning/complete/ so the top-level folder remains in-flight only.
# Shared Interaction Dialogs Plan
Status: **implemented and validated** for version 0.22.71. Full browser suite passes **1,449 / 1,449**.
docs/module-planning/json-import-security-hardening-plan.md:4
- This planning note is marked "implemented and validated". Per docs/module-planning/README.md, shipped plans should be moved under docs/module-planning/complete/ so the top-level folder remains in-flight only.
# JSON Import Security Hardening Plan
Status: **implemented and validated** for version 0.22.71.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 18 changed files in this pull request and generated 1 comment.
Files not reviewed (1)
- js/changelog.js: Generated file
Comments suppressed due to low confidence (4)
sizer/sizer.js:5154
escapeHtmlSizer()returns an empty string for any falsy value, so a workload name of numeric0(possible from untrusted imports) would render as blank. Coerce the name to a string before callingescapeHtmlSizer()so falsy-but-meaningful values are preserved.
${escapeHtmlSizer(w.name == null ? '' : w.name)}
docs/module-planning/shared-interactions-plan.md:3
- This planning note is marked "implemented and validated" for v0.22.71. To keep
docs/module-planning/focused on in-flight work, move completed plans underdocs/module-planning/complete/as part of the release PR.
Status: **implemented and validated** for version 0.22.71. Full browser suite passes **1,454 / 1,454**.
docs/module-planning/json-import-security-hardening-plan.md:3
- This planning note is marked "implemented and validated" for v0.22.71. To keep
docs/module-planning/focused on in-flight work, move completed plans underdocs/module-planning/complete/as part of the release PR.
Status: **implemented and validated** for version 0.22.71.
js/script.js:9360
applyKnownDesignerState()is declared but not referenced anywhere (and the import path now usesprepareDesignerImport()+Object.assign(state, prepared.state)). Keeping an unused helper here increases maintenance surface and can drift from the actual import behavior.
function applyKnownDesignerState(importedState) {
const safeKeys = new Set(Object.keys(getInitialWizardState()));
const skippedKeys = [];
Object.keys(importedState).forEach(key => {
if (safeKeys.has(key)) {
state[key] = importedState[key];
} else {
skippedKeys.push(key);
}
});
return skippedKeys;
}
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 18 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- js/changelog.js: Generated file
Comments suppressed due to low confidence (5)
sizer/sizer.js:5148
- In renderWorkloads(), invalid or string-valued workload IDs are coerced to a numeric
actionId, but the underlyingworkloadsentries are not updated. This means edit/clone/delete buttons can become no-ops (e.g.,actionId === -1orw.id === '1'but handler receives1). Since this code path is explicitly hardening untrusted state, it should also normalizew.idto the safe integer so handlers can still find the workload while remaining injection-safe.
const numericId = Number(w.id);
const actionId = Number.isSafeInteger(numericId) && numericId >= 0 ? numericId : -1;
sizer/sizer.js:8454
- loadSizerFromURL() bounds the encoded query parameter length, but
_shareNameis still rendered intoinnerHTMLwithout any length cap. A crafted shared URL can keepconfigunder the 12k cap while still injecting a very large_shareNamethat gets escaped but can degrade UX (very large banner text / layout churn). Consider applying the same 100-char bound used by the Share dialog before rendering.
if (configParam.length > MAX_SHARED_CONFIG_CHARS) {
console.warn('Shared configuration exceeds the supported URL size.');
return false;
}
js/script.js:9360
- applyKnownDesignerState() is newly added but never referenced. Keeping unused helpers in this file makes the import pathway harder to audit (especially in a security-hardening PR) and risks future divergence from the actual import logic.
function applyKnownDesignerState(importedState) {
const safeKeys = new Set(Object.keys(getInitialWizardState()));
const skippedKeys = [];
Object.keys(importedState).forEach(key => {
if (safeKeys.has(key)) {
docs/module-planning/shared-interactions-plan.md:3
- These planning notes are marked "implemented and validated" for v0.22.71. Per the repo’s planning-note workflow, completed plans should be moved under
docs/module-planning/complete/so the top-level folder stays focused on in-flight work.
# Shared Interaction Dialogs Plan
Status: **implemented and validated** for version 0.22.71. Full browser suite passes **1,454 / 1,454**.
docs/module-planning/json-import-security-hardening-plan.md:3
- This planning note is marked "implemented and validated" for v0.22.71. Per the repo’s planning-note workflow, completed plans should be moved under
docs/module-planning/complete/sodocs/module-planning/only contains active/in-flight plans.
# JSON Import Security Hardening Plan
Status: **implemented and validated** for version 0.22.71.
Summary
Treats imported/shared browser configuration as untrusted input and publishes the fixes as version 0.22.71.
html-validateand its transitive tree as development-only and changes HTML CI installation tonpm ciDependabot alert #21 remains temporarily open because the listed
brace-expansion@5.0.8fix is not yet available from npm. The audit gate continues to permit only that exact advisory and blocks every other high or critical finding.A strict Content Security Policy is deferred because the current static site relies on inline scripts and event handlers; adopting CSP requires a separate staged migration.
Validation
npm cinpx eslint "js/*.js" "arm/*.js" "report/*.js" "sizer/*.js" "switch-config/**/*.js" "docs/outbound-connectivity/*.js"(0 errors; 2 pre-existing indentation warnings)npx html-validate "**/*.html"node scripts/run-tests.js(1,454/1,454 passed)node scripts/check-npm-audit.jsnode scripts/smoke-test-pptx.jsgit diff --check