mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-10-19 06:07:21 +02:00
Add isolated artifact owner consent page
This commit is contained in:
1 parent
7b60979c96
commit
e4334bc9a7
10 files changed
+443
-3
No files matched your search
@@ -0,0 +1,127 @@
|
||||
require("ts-node/register/transpile-only");
|
||||
const { expect } = require("chai");
|
||||
const express = require("express");
|
||||
const http = require("http");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
const {
|
||||
createReviewConsentPage,
|
||||
isReviewConsentPagePath,
|
||||
} = require("../src/server/review-consent-page");
|
||||
|
||||
describe("isolated review consent page", function () {
|
||||
let server, directory, manifest;
|
||||
beforeEach(async function () {
|
||||
directory = fs.mkdtempSync(path.join(os.tmpdir(), "review-page-"));
|
||||
manifest = path.join(directory, "manifest.json");
|
||||
const app = express();
|
||||
const page = createReviewConsentPage(manifest);
|
||||
app.use((req, res) =>
|
||||
isReviewConsentPagePath(req.path)
|
||||
? page(req, res)
|
||||
: res.status(418).end(),
|
||||
);
|
||||
server = await new Promise((resolve) => {
|
||||
const s = app.listen(0, "127.0.0.1", () => resolve(s));
|
||||
});
|
||||
});
|
||||
afterEach(async function () {
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
fs.rmSync(directory, { recursive: true, force: true });
|
||||
});
|
||||
function get(url, method = "GET") {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = http.request(
|
||||
{
|
||||
hostname: "127.0.0.1",
|
||||
port: server.address().port,
|
||||
path: url,
|
||||
method,
|
||||
},
|
||||
(res) => {
|
||||
let body = "";
|
||||
res.on("data", (chunk) => {
|
||||
body += chunk;
|
||||
});
|
||||
res.on("end", () =>
|
||||
resolve({ status: res.statusCode, headers: res.headers, body }),
|
||||
);
|
||||
},
|
||||
);
|
||||
req.on("error", reject);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
it("serves only first-party assets with no inline scripts or external frames", async function () {
|
||||
const response = await get("/review-link");
|
||||
expect(response.status).equal(200);
|
||||
expect(response.headers["cache-control"]).equal("no-store");
|
||||
expect(response.headers["referrer-policy"]).equal("no-referrer");
|
||||
expect(response.headers["x-frame-options"]).equal("DENY");
|
||||
expect(response.headers["content-security-policy"])
|
||||
.include("script-src 'self'")
|
||||
.include("frame-ancestors 'none'");
|
||||
expect(response.body).include('name="review-consent-page"');
|
||||
expect(response.body).not.match(/https?:|kofi|ko-fi|<iframe|<script\s*>/i);
|
||||
expect(response.body.match(/<script /g)).length(2);
|
||||
expect(response.body)
|
||||
.include("/script/core.min.js")
|
||||
.include("/script/vendor.min.js");
|
||||
});
|
||||
it("reserves encoded, case and slash aliases and rejects query strings and writes", async function () {
|
||||
for (const url of [
|
||||
"/review-link/",
|
||||
"/REVIEW-LINK",
|
||||
"/%72eview-link",
|
||||
"/review-link?token=secret",
|
||||
"/review-link///",
|
||||
]) {
|
||||
const response = await get(url);
|
||||
expect(response.status, url).equal(404);
|
||||
expect(response.body).not.include("<html");
|
||||
expect(response.headers["referrer-policy"]).equal("no-referrer");
|
||||
}
|
||||
expect((await get("/review-link", "POST")).status).equal(404);
|
||||
expect((await get("/review-link-other")).status).equal(418);
|
||||
expect(isReviewConsentPagePath("/%")).equal(false);
|
||||
});
|
||||
it("accepts hashed filenames but rejects manifest URL and HTML injection", async function () {
|
||||
fs.writeFileSync(
|
||||
manifest,
|
||||
JSON.stringify({
|
||||
"core.min.js": "core.abc123.js",
|
||||
"vendor.min.js": "//evil.test/x.js",
|
||||
"all.min.css": 'x.css" onload="alert(1)',
|
||||
}),
|
||||
);
|
||||
const response = await get("/review-link");
|
||||
expect(response.body)
|
||||
.include("/script/core.abc123.js")
|
||||
.include("/script/vendor.min.js")
|
||||
.include("/css/all.min.css");
|
||||
expect(response.body).not.include("evil.test").not.include("onload");
|
||||
fs.writeFileSync(manifest, "null");
|
||||
expect((await get("/review-link")).status).equal(200);
|
||||
fs.writeFileSync(manifest, "{");
|
||||
expect((await get("/review-link")).status).equal(200);
|
||||
});
|
||||
it("serves HEAD without a body", async function () {
|
||||
const response = await get("/review-link", "HEAD");
|
||||
expect(response.status).equal(200);
|
||||
expect(response.body).equal("");
|
||||
});
|
||||
it("measures 100 isolated page responses", async function () {
|
||||
const times = [];
|
||||
const { performance } = require("perf_hooks");
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const start = performance.now();
|
||||
expect((await get('/review-link')).status).equal(200);
|
||||
times.push(performance.now() - start);
|
||||
}
|
||||
times.sort((a, b) => a - b);
|
||||
const output = require("process").env.TEST_REVIEW_PAGE_PERF_REPORT;
|
||||
if (output) fs.writeFileSync(output, JSON.stringify({ requests: 100, workload: 'isolated HTML shell, loopback HTTP, no browser or provider', medianMs: times[49], p95Ms: times[94] }, null, 2) + '\n');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,130 @@
|
||||
const { expect } = require("chai");
|
||||
const { JSDOM, VirtualConsole } = require("jsdom");
|
||||
const fs = require("fs");
|
||||
const { URL } = require("url");
|
||||
const path = require("path");
|
||||
const { setTimeout: delay } = require("timers/promises");
|
||||
const bundles = ["core.min.js", "vendor.min.js"].map(name => fs.readFileSync(path.join(__dirname, "../public/script", name), "utf8"));
|
||||
const token = "3".repeat(64);
|
||||
const fragment = `clientId=${"1".repeat(32)}&intentId=${"2".repeat(32)}&token=${token}`;
|
||||
const csrf = "a".repeat(64);
|
||||
const preview = () => ({ ticket: 'private-ticket', repositoryName: 'Synthetic repository', policy: { version: 1, access: 'restricted-review', retainUntil: '2099-01-01T00:00:00Z' }, expiresAt: new Date(Date.now() + 60000).toISOString() });
|
||||
async function browser(overrides = {}, hash = fragment) {
|
||||
const errors = [], requests = [];
|
||||
const vc = new VirtualConsole();
|
||||
vc.on('jsdomError', error => errors.push(error.message));
|
||||
vc.on('error', error => errors.push(String(error)));
|
||||
const dom = new JSDOM('<!doctype html><html><head><meta name="review-consent-page" content="1"></head><body><div id="app"></div></body></html>', { url: 'https://anonymous.example.test/review-link#' + hash, runScripts: 'dangerously', pretendToBeVisual: true, virtualConsole: vc });
|
||||
const w = dom.window;
|
||||
w.matchMedia = () => ({ matches: false });
|
||||
w.HTMLCanvasElement.prototype.getContext = () => null;
|
||||
w.fetch = async (url, options = {}) => {
|
||||
const pathname = new URL(url).pathname;
|
||||
const request = { pathname, ...options, payload: options.body ? JSON.parse(options.body) : null };
|
||||
requests.push(request);
|
||||
let data;
|
||||
if (overrides[pathname]) data = await overrides[pathname](request);
|
||||
else if (pathname === '/api/user') data = { username: 'owner' };
|
||||
else if (pathname === '/api/review-consent/csrf') data = { csrf };
|
||||
else if (pathname === '/api/review-consent/preview') data = preview();
|
||||
else if (pathname === '/api/review-consent/confirm') data = { requestId: request.payload.requestId, confirmedAt: new Date().toISOString() };
|
||||
else data = [];
|
||||
const status = data?.__status || 200;
|
||||
return { ok: status < 400, status, headers: { get: () => 'application/json' }, text: async () => JSON.stringify(data?.__status ? data.body : data) };
|
||||
};
|
||||
bundles.forEach(bundle => w.eval(bundle));
|
||||
const app = w.anonymousApp;
|
||||
await app.router.isReady(); await delay(30);
|
||||
const document = w.document;
|
||||
const click = async selector => { document.querySelector(selector).click(); await delay(30); };
|
||||
return { w, app, document, errors, requests, click,
|
||||
async preview() { const input = document.querySelector('#review-repository'); input.value = 'synthetic-repo'; input.dispatchEvent(new w.Event('input', { bubbles: true })); await click('button[type=submit]'); },
|
||||
async accept() { for (const input of document.querySelectorAll('input[type=checkbox]')) { input.click(); } await delay(5); },
|
||||
close() { app.app.unmount(); w.close(); },
|
||||
};
|
||||
}
|
||||
describe('review consent browser page', function () {
|
||||
this.timeout(10000);
|
||||
let b;
|
||||
afterEach(() => b?.close());
|
||||
it('scrubs the fragment before requests and keeps secrets out of rendered state and storage', async function () {
|
||||
b = await browser();
|
||||
expect(b.w.location.href).equal('https://anonymous.example.test/review-link');
|
||||
expect(JSON.stringify(b.w.history.state)).not.include(token);
|
||||
await b.preview();
|
||||
expect(b.document.body.textContent).include('Synthetic repository');
|
||||
expect(b.document.documentElement.outerHTML).not.include(token).not.include('private-ticket');
|
||||
expect(Object.keys(b.w.localStorage)).deep.equal(['darkMode']);
|
||||
expect(JSON.stringify(b.w.localStorage)).not.include(token).not.include('private-ticket');
|
||||
expect(b.w.sessionStorage.length).equal(0);
|
||||
expect(b.document.querySelector('.app-header')).equal(null);
|
||||
expect(b.requests.find(r => r.pathname.endsWith('/preview')).payload.intent.token).equal(token);
|
||||
expect(b.errors).deep.equal([]);
|
||||
});
|
||||
it('requires both approvals and preserves the confirmation identity after response loss', async function () {
|
||||
let attempts = 0;
|
||||
b = await browser({ '/api/review-consent/confirm': request => { if (++attempts === 1) throw new Error('lost response'); return { requestId: request.payload.requestId, confirmedAt: new Date().toISOString() }; } });
|
||||
await b.preview();
|
||||
expect(b.document.querySelector('button[type=button]').disabled).equal(true);
|
||||
await b.accept(); await b.click('button[type=button]');
|
||||
expect(b.document.body.textContent).include('Retry confirmation');
|
||||
expect(b.document.querySelector('#review-repository').disabled).equal(true);
|
||||
await b.click('button[type=button]');
|
||||
const writes = b.requests.filter(r => r.pathname.endsWith('/confirm'));
|
||||
expect(writes).length(2); expect(writes[0].payload).deep.equal(writes[1].payload);
|
||||
expect(b.document.body.textContent).include('Consent recorded. Artifact linking is not complete.');
|
||||
});
|
||||
it('does not send a preview after departure during the CSRF request', async function () {
|
||||
let resolve;
|
||||
b = await browser({ '/api/review-consent/csrf': () => new Promise(yes => { resolve = yes; }) });
|
||||
await b.preview(); b.app.app.unmount(); resolve({ csrf }); await delay(30);
|
||||
expect(b.requests.filter(r => r.pathname.endsWith('/preview'))).length(0);
|
||||
expect(b.requests.find(r => r.pathname.endsWith('/csrf')).signal.aborted).equal(true);
|
||||
});
|
||||
it('discards a late preview after account change', async function () {
|
||||
let resolve;
|
||||
b = await browser({ '/api/review-consent/preview': () => new Promise(yes => { resolve = yes; }) });
|
||||
await b.preview(); b.app.state.user = { username: 'someone-else' }; await delay(5); resolve(preview()); await delay(30);
|
||||
expect(b.document.body.textContent).include('Your account changed');
|
||||
expect(b.document.body.textContent).not.include('Synthetic repository');
|
||||
});
|
||||
it('rejects duplicate or missing fragment fields without making consent requests', async function () {
|
||||
b = await browser({}, fragment + '&token=' + token); await b.preview();
|
||||
expect(b.document.body.textContent).include('missing or invalid');
|
||||
expect(b.requests.some(r => r.pathname.startsWith('/api/review-consent'))).equal(false);
|
||||
expect(b.w.location.hash).equal('');
|
||||
});
|
||||
it('shows unavailable endpoints and keeps the intent request ID on preview retry', async function () {
|
||||
let attempts = 0;
|
||||
b = await browser({ '/api/review-consent/preview': () => ++attempts === 1 ? { __status: 404, body: {} } : preview() });
|
||||
await b.preview(); expect(b.document.body.textContent).include('not available'); await b.preview();
|
||||
const requests = b.requests.filter(r => r.pathname.endsWith('/preview'));
|
||||
expect(requests[0].payload.intent.requestId).equal(requests[1].payload.intent.requestId);
|
||||
});
|
||||
it('does not approve an expired preview', async function () {
|
||||
b = await browser({ '/api/review-consent/preview': () => ({ ...preview(), expiresAt: new Date(Date.now() + 1000).toISOString() }) });
|
||||
await b.preview(); await b.accept(); await delay(1050); await b.click('button[type=button]');
|
||||
expect(b.requests.some(r => r.pathname.endsWith('/confirm'))).equal(false);
|
||||
expect(b.document.body.textContent).include('preview expired');
|
||||
});
|
||||
it('requires sign-in without retaining the handoff in browser storage', async function () {
|
||||
b = await browser({ '/api/user': () => null });
|
||||
expect(b.document.querySelector('#review-repository')).equal(null);
|
||||
expect(b.document.body.textContent).include('Sign in first');
|
||||
expect(b.w.location.hash).equal('');
|
||||
expect(b.w.sessionStorage.length).equal(0);
|
||||
expect(b.requests.some(r => r.pathname.startsWith('/api/review-consent'))).equal(false);
|
||||
});
|
||||
it('rejects malformed policy previews and mismatched confirmation receipts', async function () {
|
||||
let attempts = 0;
|
||||
b = await browser({
|
||||
'/api/review-consent/preview': () => ++attempts === 1 ? { ...preview(), policy: { ...preview().policy, access: 'unsupported' } } : preview(),
|
||||
'/api/review-consent/confirm': () => ({ requestId: 'wrong', confirmedAt: new Date().toISOString() }),
|
||||
});
|
||||
await b.preview(); expect(b.document.querySelector('input[type=checkbox]')).equal(null);
|
||||
await b.preview(); await b.accept(); await b.click('button[type=button]');
|
||||
expect(b.document.body.textContent).include('confirmation was not verified');
|
||||
expect(b.document.body.textContent).not.include('Consent recorded.');
|
||||
});
|
||||
|
||||
});
|
||||
Reference in new issue
Block a user