[grade=B] DC-131/132/133 install from any Git host
Codex source gate: urn:ump:iw2tbbe6mssyl5divymmwo42ael65sbfrciztvyowo3zhrbtircq Generated assets gate: urn:ump:hintflviuxfpeidth42ry5fi4lwqsjhkxipzspfmk7vrvfc2cnqq
This commit is contained in:
@@ -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) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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('<script>'), 'escaped script tag present as inert text');
|
||||
assert.ok(html.includes('">'), '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) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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"/);
|
||||
});
|
||||
Reference in New Issue
Block a user