Skip to content

Release v0.22.71 with browser security hardening - #273

Merged
NeilBird merged 7 commits into
mainfrom
Release
Jul 29, 2026
Merged

Release v0.22.71 with browser security hardening#273
NeilBird merged 7 commits into
mainfrom
Release

Conversation

@NeilBird

@NeilBird NeilBird commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

Treats imported/shared browser configuration as untrusted input and publishes the fixes as version 0.22.71.

  • closes Sizer file, localStorage, and Share-as-URL DOM XSS paths by escaping rendered values and normalizing imported IDs
  • restricts Designer imports to known state keys and escapes all state-derived printable HTML
  • migrates recognized legacy Designer values before apply and rejects unsupported state without changing the current design
  • identifies Sizer and Designer exports used at the wrong import entry point, then opens centered guidance with a direct action to the correct tool
  • adds 5 MB file and machine-paste limits, shared URL/workload/name bounds, hardware bounds, and CSV formula neutralization
  • documents increment-only Firebase counter rules applied separately by the repository owner
  • classifies html-validate and its transitive tree as development-only and changes HTML CI installation to npm ci
  • unifies Designer and Sizer toasts and dialogs for Reset, Start Over, Share URL, and Designer Export, with accessible focus handling, responsive mobile actions, and a Safari 14 compatibility path
  • adds direct regressions for malicious markup, dangerous keys, legacy migration, cross-tool detection, machine paste bounds, import normalization, spreadsheet formula prefixes, and shared interaction accessibility

Dependabot alert #21 remains temporarily open because the listed brace-expansion@5.0.8 fix 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 ci
  • npx 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.js
  • node scripts/smoke-test-pptx.js
  • desktop and 375 px mobile browser checks for centered cross-tool guidance, responsive actions, reciprocal navigation, and the non-native Safari dialog path
  • git diff --check
  • planning-note privacy scan (0 matches)
  • Copilot review: all inline threads addressed and resolved; applicable suppressed observations also addressed

Copilot AI review requested due to automatic review settings July 29, 2026 08:16

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-validate dev-only; CI uses npm 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

Comment thread js/analytics.js Outdated
Comment thread js/script.js
Comment thread sizer/sizer.js
@NeilBird NeilBird changed the title Harden browser configuration imports ODIN v0.22.71 - Security review, harden browser configuration import code paths Jul 29, 2026
Copilot AI review requested due to automatic review settings July 29, 2026 09:31
@NeilBird NeilBird changed the title ODIN v0.22.71 - Security review, harden browser configuration import code paths Release v0.22.71 with browser security hardening Jul 29, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread sizer/sizer.js Outdated
Copilot AI review requested due to automatic review settings July 29, 2026 09:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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() interpolates w.type directly into a class="..." attribute. Because workloads can be restored from untrusted localStorage (see resumeSizerState()), a tampered w.type containing quotes can break out of the attribute and inject new attributes/handlers. Also, actionId currently 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 entire details string in renderWorkloads() 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 (see docs/module-planning/README.md).
# JSON Import Security Hardening Plan

Status: **implemented and validated** for version 0.22.71.

Copilot AI review requested due to automatic review settings July 29, 2026 09:54

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 value 0 as empty, which can silently drop an imported or programmatic workload name. Coerce to string while preserving 0/falsey-but-valid values before escaping.
                        ${escapeHtmlSizer(w.name || '')}

sizer/sizer.js:9874

  • isDesignerExportPayload() currently treats any JSON object with a state object as a Designer export. This can misclassify unrelated JSON (or future Sizer payloads that happen to include state) and block valid imports with a misleading message. Make the detection more specific to the actual Designer export envelope (e.g., require a string version plus state).
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.

Copilot AI review requested due to automatic review settings July 29, 2026 10:13

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot AI review requested due to automatic review settings July 29, 2026 10:24

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 numeric 0 (possible from untrusted imports) would render as blank. Coerce the name to a string before calling escapeHtmlSizer() 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 under docs/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 under docs/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 uses prepareDesignerImport() + 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;
}

Comment thread sizer/sizer.js Outdated
Copilot AI review requested due to automatic review settings July 29, 2026 10:35

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 underlying workloads entries are not updated. This means edit/clone/delete buttons can become no-ops (e.g., actionId === -1 or w.id === '1' but handler receives 1). Since this code path is explicitly hardening untrusted state, it should also normalize w.id to 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 _shareName is still rendered into innerHTML without any length cap. A crafted shared URL can keep config under the 12k cap while still injecting a very large _shareName that 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/ so docs/module-planning/ only contains active/in-flight plans.
# JSON Import Security Hardening Plan

Status: **implemented and validated** for version 0.22.71.

@NeilBird
NeilBird merged commit cf1b6ec into main Jul 29, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants