[grade=B] DC-131/132/133 install from any Git host
CI / Test & Lint (push) Waiting to run
CI / Security audit (push) Waiting to run

Codex source gate: urn:ump:iw2tbbe6mssyl5divymmwo42ael65sbfrciztvyowo3zhrbtircq

Generated assets gate: urn:ump:hintflviuxfpeidth42ry5fi4lwqsjhkxipzspfmk7vrvfc2cnqq
This commit is contained in:
DashCaddy Polish Loop
2026-09-14 20:10:14 -07:00
parent 7557b49b5b
commit f9cbb13a3d
7 changed files with 1119 additions and 280 deletions
+195
View File
@@ -0,0 +1,195 @@
'use strict';
/**
* DC-131/133 deploys Install tab tests.
*
* Pins the DOM-XSS escaping on the Gitea repo picker: remote repo fields
* (url, id, full_name, description) are UNTRUSTED — a hostile Gitea
* instance controls them — so they must be HTML-escaped before they reach
* innerHTML. We load status/js/deploys.js in a sandboxed VM with a minimal
* mocked DOM (same pattern as share-modal.test.js) and drive the exposed
* window.__dc133_buildRepoOptions() with hostile payloads.
*
* Also verifies the token input is type="password" (not echoed to screen)
* and that the modal carries no github.com-only assumptions.
*/
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const test = require('node:test');
const assert = require('node:assert/strict');
function findTarget() {
// Prefer the panel (ui) file; the routes/deploys.js proxy file is a
// different module (CommonJS, jest-side) and must not match this scan.
const candidates = [
path.join(__dirname, '..', 'js', 'deploys.js'),
path.join(__dirname, 'deploys.js'),
path.join(__dirname, 'ui-deploys.js'),
];
for (const p of candidates) {
try { if (fs.statSync(p).isFile()) return p; } catch (_) { /* keep looking */ }
}
const dir = __dirname;
let entries = [];
try { entries = fs.readdirSync(dir); } catch (_) { return null; }
const match = entries.find(e => e.endsWith('_ui-deploys.js') || e.endsWith('-ui-deploys.js'));
return match ? path.join(dir, match) : null;
}
const SOURCE_PATH = findTarget();
if (!SOURCE_PATH) {
throw new Error('Cannot find ui-deploys.js (panel bundle source). Searched ' + __dirname + ' and ../js/.');
}
function buildFakeDom() {
const elements = new Map();
function makeEl(id) {
return {
id,
value: '',
textContent: '',
innerHTML: '',
style: {},
dataset: {},
classList: {
_set: new Set(),
add(c) { this._set.add(c); },
remove(c) { this._set.delete(c); },
toggle(c, on) { if (on) this._set.add(c); else this._set.delete(c); },
contains(c) { return this._set.has(c); },
},
disabled: false,
addEventListener() {},
appendChild() {},
querySelectorAll() { return []; },
selectedOptions: [],
setAttribute() {},
getAttribute() { return null; },
};
}
const knownIds = [
'deploys-modal', 'dep-gh-gitea', 'dep-gh-gitea-host', 'dep-gh-gitea-token',
'dep-gh-anonymous', 'dep-gh-url', 'dep-gh-service', 'dep-gh-args', 'dep-gh-btn',
'dep-output', 'dep-services', 'dep-journal', 'dep-journal-name',
'dep-journal-btn', 'dep-status-name', 'dep-status-btn',
'dep-repo-select', 'dep-close', 'dep-deploy-btn',
];
for (const id of knownIds) elements.set(id, makeEl(id));
return {
_elements: elements,
body: { insertAdjacentHTML() {}, appendChild() {} },
getElementById(id) { return elements.get(id) || null; },
createElement() { return makeEl('created'); },
addEventListener() {},
querySelectorAll() { return []; },
readyState: 'complete',
};
}
function buildSandbox() {
const dom = buildFakeDom();
const windowStub = {
escapeHtml: (s) => String(s == null ? '' : s)
.replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])),
renderApps: undefined,
renderGrid: undefined,
APPS: [],
};
const sandbox = {
window: windowStub,
document: dom,
fetch: () => Promise.resolve({ status: 200, json: async () => ({ success: true, rows: [], repos: [], services: [] }) }),
URL,
location: { origin: 'https://status.sami' },
setTimeout,
clearTimeout,
navigator: {},
injectModal: () => {},
wireModal: () => {},
showNotification: () => {},
console,
};
vm.createContext(sandbox);
return { sandbox, dom, windowStub };
}
function loadModule() {
const { sandbox, dom, windowStub } = buildSandbox();
vm.runInContext(fs.readFileSync(SOURCE_PATH, 'utf8'), sandbox, { filename: SOURCE_PATH });
return { dom, windowStub };
}
test('deploys.js loads in sandbox and exposes the DC-133 option builder', () => {
const { windowStub } = loadModule();
assert.equal(typeof windowStub.__dc133_buildRepoOptions, 'function');
});
test('repo options escape hostile full_name/description/url/id payloads', () => {
const { windowStub } = loadModule();
const build = windowStub.__dc133_buildRepoOptions;
const hostile = [{
url: 'https://evil.example/"><script>alert(1)</script>/x',
id: 'x" onmouseover="alert(2)',
full_name: '<script>alert(3)</script>',
description: '"><img src=x onerror=alert(4)>',
}];
const html = build(hostile);
// Security property: the hostile payloads' raw attack vectors must not
// survive — tags cannot open, quotes cannot delimit attributes. (The
// output legitimately contains its own <option> elements; what must be
// absent is any raw form of the injected values.)
assert.equal(html.includes('<script'), false, 'raw <script from payload must not survive');
assert.equal(html.includes('<img'), false, 'raw <img from payload must not survive');
assert.equal(html.includes('"><'), false, 'quote-angle injection delimiter must not survive');
assert.equal(html.includes('onmouseover="'), false, 'raw quoted attribute from payload must not survive');
assert.ok(html.includes('&lt;script&gt;'), 'escaped script tag present as inert text');
assert.ok(html.includes('&quot;&gt;'), 'escaped quote-angle present');
});
test('repo options builder is safe with an injected escFn too (no bypass)', () => {
const { windowStub } = loadModule();
const build = windowStub.__dc133_buildRepoOptions;
const html = build([{ url: 'u"><svg onload=alert(9)>', id: 'i', full_name: 'n', description: 'd' }],
(s) => String(s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])));
assert.equal(html.includes('<svg'), false, 'raw <svg from payload must not survive');
assert.equal(html.includes('"><'), false, 'quote-angle delimiter must not survive');
});
test('empty and null repos arrays produce just the placeholder option', () => {
const { windowStub } = loadModule();
const build = windowStub.__dc133_buildRepoOptions;
assert.ok(build([]).includes('choose a repo'));
assert.ok(build(null).includes('choose a repo'));
});
test('token input is type=password in the modal markup', () => {
const source = fs.readFileSync(SOURCE_PATH, 'utf8');
const m = source.match(/id="dep-gh-gitea-token"[^>]*>/);
assert.ok(m, 'token input exists');
assert.ok(/type="password"/.test(m[0]), 'token input must be type=password, got: ' + m[0]);
});
test('install modal has no github.com-only placeholders (any-host UX)', () => {
const source = fs.readFileSync(SOURCE_PATH, 'utf8');
assert.equal(source.includes('https://github.com/owner/repo'), false,
'placeholder must not suggest github-only URLs');
});
test('request-token helper preserves explicit anonymous versus omitted', () => {
const { windowStub } = loadModule();
const token = windowStub.__dc133_requestToken;
assert.equal(typeof token, 'function');
assert.equal(token(true, 'typed-secret'), '',
'anonymous checkbox wins and emits explicit empty string');
assert.equal(token(false, ' typed-secret '), 'typed-secret');
assert.equal(token(false, ''), undefined,
'blank field without anonymous checkbox omits token (fleet fallback allowed)');
assert.equal(token(false, ' '), undefined);
});
test('anonymous checkbox exists in modal markup', () => {
const source = fs.readFileSync(SOURCE_PATH, 'utf8');
assert.match(source, /id="dep-gh-anonymous"[^>]*type="checkbox"/);
});