'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/">/x', id: 'x" onmouseover="alert(2)', full_name: '', description: '">', }]; 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