Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids",
"test:browser-security-headers": "node --test scripts/browser-security-headers.test.js"
"test:browser-security-headers": "node --test --test-concurrency=2 scripts/browser-security-headers.test.js security/browser-headers/rust-browser-source.test.cjs"
},
"dependencies": {
"@docusaurus/core": "3.10.2",
Expand Down
5 changes: 5 additions & 0 deletions security/browser-headers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ a browser compatibility check. No current-artifact browser run or provider stage
is authorized by this source proposal. Follow `rust-stage-proposal.md` for the
exact input bindings, ownership, proposed checks and rollback decision.

`rust-local-browser-plan.md` and `rust-local-browser-packet.json` define the
separately reviewable, unrun local browser/telemetry packet. Normal CI exercises
only its inert source controls. The optional browser entrypoint requires its
own exact run admission; it makes no provider or production request.

## Rust-only staged acceptance and rollback

1. Bind the accepted package, source/tooling revisions, protected artifact ZIP
Expand Down
1 change: 1 addition & 0 deletions security/browser-headers/runtime/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules/
42 changes: 42 additions & 0 deletions security/browser-headers/runtime/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions security/browser-headers/runtime/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "durable-workflow-rust-browser-check",
"private": true,
"version": "0.0.0",
"dependencies": {
"playwright": "1.64.0"
}
}
133 changes: 133 additions & 0 deletions security/browser-headers/rust-browser-inputs.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
const assert = require('node:assert/strict');
const crypto = require('node:crypto');
const fs = require('node:fs');
const path = require('node:path');

const ORIGIN = 'https://rust.durable-workflow.com';
const BEACON = 'https://static.cloudflareinsights.com/beacon.min.js';
const RUM = 'https://cloudflareinsights.com/cdn-cgi/rum';
const PROMOTION = 'https://cloud.durable-workflow.com/early-access/promotion-events';
const IMAGE_ORIGINS = new Set(['https://github.com', 'https://img.shields.io', 'https://sonarcloud.io']);
const MIME = {'.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png', '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf'};
const digest = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');

function verifyPacketFiles(directory) {
const packet = JSON.parse(fs.readFileSync(path.join(directory, 'rust-local-browser-packet.json')));
assert.equal(packet.run_status, 'not-run');
for (const [name, hash] of Object.entries(packet.files)) {
assert.ok(!path.isAbsolute(name) && !name.split('/').includes('..'), 'Unsafe packet path');
assert.equal(digest(fs.readFileSync(path.join(directory, name))), hash, 'Packet member changed');
}
return packet;
}

function loadInputs(root, packet, inventory) {
assert.equal(packet.rule.enabled, false);
assert.equal(packet.rollout_approved, false);
assert.equal(packet.compatibility_accepted, false);
assert.equal(inventory.origin, ORIGIN);
assert.equal(packet.inventory_sha256, digest(JSON.stringify(inventory, null, 2) + '\n'));
assert.deepEqual(packet.input_artifact, inventory.input_artifact);
assert.equal(packet.source_preparation, null);
assert.equal(inventory.source_preparation, null);
assert.ok(fs.lstatSync(root).isDirectory(), 'Artifact root must be a regular directory');
const files = new Map();
let bytes = 0;
function visit(directory) {
for (const entry of fs.readdirSync(directory, {withFileTypes: true})) {
const absolute = path.join(directory, entry.name);
assert.ok(!entry.isSymbolicLink(), 'Artifact symlinks are refused');
if (entry.isDirectory()) visit(absolute);
else {
assert.ok(entry.isFile(), 'Artifact special files are refused');
const relative = path.relative(root, absolute).split(path.sep).join('/');
const size = fs.statSync(absolute).size;
bytes += size;
assert.ok(bytes <= 64 * 1024 * 1024 && files.size < 1000, 'Artifact exceeds the local input bound');
files.set(relative, fs.readFileSync(absolute));
}
}
}
visit(root);
assert.equal(files.size, inventory.files.length, 'Artifact file count changed');
const seen = new Set();
for (const record of inventory.files) {
assert.ok(!seen.has(record.path), 'Duplicate inventory path');
seen.add(record.path);
const content = files.get(record.path);
assert.ok(content, 'Inventoried artifact file is missing');
assert.equal(content.length, record.bytes, 'Artifact file size changed');
assert.equal(digest(content), record.sha256, 'Artifact file digest changed');
}
assert.deepEqual(JSON.parse(files.get('__build.json')), inventory.build_identity);
return {files, packet, inventory, headers: Object.fromEntries(Object.entries(packet.rule.action_parameters.headers).map(([name, record]) => [name, record.value]))};
}

function promotionEvent(request) {
assert.equal(request.method, 'POST', 'Promotion request method changed');
const headers = Object.fromEntries(Object.entries(request.headers).map(([name, value]) => [name.toLowerCase(), value]));
assert.ok(!Object.hasOwn(headers, 'authorization'), 'Local telemetry cannot carry authorization');
assert.ok(!Object.hasOwn(headers, 'cookie'), 'Local telemetry cannot carry cookies');
assert.match(headers['content-type'] || '', /^text\/plain(?:;|$)/);
assert.ok(typeof request.body === 'string' && request.body.length < 256, 'Promotion body exceeds its bound');
const event = JSON.parse(request.body);
assert.deepEqual(Object.keys(event).toSorted((left, right) => left.localeCompare(right, 'en')), ['event', 'source']);
assert.equal(event.source, 'sdk-rust-reference');
assert.ok(['impression', 'click'].includes(event.event), 'Unknown promotion event');
return event.event;
}

function artifactResponse(input, url) {
const relative = decodeURIComponent(url.pathname).replace(/^\//, '');
if (relative === '__frame_probe') return {kind: 'frame-probe', status: 200, headers: {'content-type': 'text/html'}, body: '<!doctype html><title>Framing control</title>'};
if (relative === '__telemetry_fixture') return {kind: 'promotion-dom-fixture', status: 200, headers: {...input.headers, 'content-type': 'text/html'}, body: '<!doctype html><title>Synthetic promotion control</title><aside data-promotion-source="sdk-rust-reference"><a data-promotion-action="early-access" href="https://cloud.durable-workflow.com/early-access">Synthetic action</a></aside><script type="module" src="/analytics/analytics.js"></script>'};
const file = [relative, relative.replace(/\/$/, '') + '/index.html', relative ? null : 'index.html'].find((name) => name && input.files.has(name));
if (!file) return {kind: 'missing', status: 404, body: ''};
return {kind: 'artifact', status: 200, headers: {...input.headers, 'content-type': MIME[path.extname(file)] || 'application/octet-stream', 'cache-control': 'no-store'}, body: input.files.get(file)};
}

function localResponse(input, request, origin = ORIGIN) {
if (origin !== ORIGIN && origin !== 'https://example.invalid') throw new Error('Unexpected local origin');
const requestHeaders = Object.fromEntries(Object.entries(request.headers).map(([name, value]) => [name.toLowerCase(), value]));
assert.ok(!Object.hasOwn(requestHeaders, 'authorization'), 'Local requests cannot carry authorization');
assert.ok(!Object.hasOwn(requestHeaders, 'cookie'), 'Local requests cannot carry cookies');
const url = new URL(request.url);
if (url.origin === origin) {
assert.equal(request.method, 'GET', 'Artifact requests must be GET');
return artifactResponse(input, url);
}
const cors = {'access-control-allow-origin': origin};
if (request.url === BEACON && request.method === 'GET') {
// This finite fixture exercises CSP loader/connect paths, never vendor code or delivery.
return {kind: 'beacon-fixture', status: 200, headers: {...cors, 'content-type': 'text/javascript'}, body: `globalThis.__localBeaconFixture = true; fetch('${RUM}', {method: 'POST', credentials: 'omit', headers: {'Content-Type': 'text/plain'}, body: 'local-csp-probe'});`};
}
if (request.url === RUM && request.method === 'POST' && request.body === 'local-csp-probe') {
return {kind: 'rum-fixture', status: 204, headers: cors, body: ''};
}
if (request.url === PROMOTION) return {kind: 'promotion-' + promotionEvent(request), status: 204, headers: cors, body: ''};
if (IMAGE_ORIGINS.has(url.origin) && request.method === 'GET' && request.resourceType === 'image') {
return {kind: 'image-fixture', status: 200, headers: {'content-type': 'image/svg+xml'}, body: '<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>'};
}
throw new Error('Request is outside the finite local response set');
}

async function installLocalRoutes(context, input, origin = ORIGIN) {
const audit = {requests: 0, kinds: {}, refused: []};
await context.routeWebSocket('**/*', (socket) => { audit.refused.push('websocket'); socket.close(); });
await context.route('**/*', async (route) => {
try {
audit.requests += 1;
assert.ok(audit.requests <= 4000, 'Local request budget exceeded');
const request = route.request();
const response = localResponse(input, {url: request.url(), method: request.method(), headers: request.headers(), body: request.postData(), resourceType: request.resourceType()}, origin);
audit.kinds[response.kind] = (audit.kinds[response.kind] || 0) + 1;
await route.fulfill({status: response.status, headers: response.headers, body: response.body});
} catch {
audit.refused.push('http');
await route.abort();
}
});
return audit;
}

module.exports = {ORIGIN, BEACON, RUM, PROMOTION, digest, verifyPacketFiles, loadInputs, localResponse, installLocalRoutes};
106 changes: 106 additions & 0 deletions security/browser-headers/rust-browser-source.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const test = require('node:test');
const {ORIGIN, BEACON, RUM, PROMOTION, digest, loadInputs, localResponse, installLocalRoutes} = require('./rust-browser-inputs.cjs');

function fixture(t) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rust-browser-source-'));
t.after(() => fs.rmSync(root, {recursive: true, force: true}));
const build = {version: 'fixture'};
const files = {'__build.json': JSON.stringify(build), 'index.html': '<h1>Fixture</h1>'};
for (const [name, value] of Object.entries(files)) fs.writeFileSync(path.join(root, name), value);
const inventory = {origin: ORIGIN, input_artifact: {artifact_id: 'fixture'}, source_preparation: null, build_identity: build, files: Object.entries(files).map(([name, content]) => ({path: name, bytes: Buffer.byteLength(content), sha256: digest(content)}))};
const packet = {rule: {enabled: false, action_parameters: {headers: {'content-security-policy': {value: "default-src 'none'"}}}}, rollout_approved: false, compatibility_accepted: false, source_preparation: null, input_artifact: inventory.input_artifact, inventory_sha256: digest(JSON.stringify(inventory, null, 2) + '\n')};
return {root, inventory, packet};
}

function request(url, options = {}) {
return {url, method: 'GET', headers: {}, body: null, resourceType: 'script', ...options};
}

test('verifies every input file and refuses drift before a browser is needed', (t) => {
const value = fixture(t);
assert.equal(loadInputs(value.root, value.packet, value.inventory).files.size, 2);
fs.writeFileSync(path.join(value.root, 'index.html'), '<h1>Changed</h1>');
assert.throws(() => loadInputs(value.root, value.packet, value.inventory), /Artifact file/);
});

test('refuses extra files, symlinks, duplicate inventory and changed approval flags', (t) => {
const value = fixture(t);
fs.writeFileSync(path.join(value.root, 'extra'), 'extra');
assert.throws(() => loadInputs(value.root, value.packet, value.inventory), /file count/);
fs.unlinkSync(path.join(value.root, 'extra'));
fs.symlinkSync(path.join(value.root, 'index.html'), path.join(value.root, 'link'));
assert.throws(() => loadInputs(value.root, value.packet, value.inventory), /symlinks/);
fs.unlinkSync(path.join(value.root, 'link'));
for (const mutate of [(p) => { p.rule.enabled = true; }, (p) => { p.rollout_approved = true; }, (p) => { p.compatibility_accepted = true; }]) {
const packet = structuredClone(value.packet); mutate(packet);
assert.throws(() => loadInputs(value.root, packet, value.inventory));
}
value.inventory.files[1] = value.inventory.files[0];
value.packet.inventory_sha256 = digest(JSON.stringify(value.inventory, null, 2) + '\n');
assert.throws(() => loadInputs(value.root, value.packet, value.inventory), /Duplicate inventory/);
});

test('serves only verified buffers and finite external fixtures', (t) => {
const value = fixture(t);
const input = loadInputs(value.root, value.packet, value.inventory);
assert.equal(localResponse(input, request(ORIGIN + '/')).body.toString(), '<h1>Fixture</h1>');
assert.equal(localResponse(input, request(ORIGIN + '/%2e%2e/private')).status, 404);
assert.equal(localResponse(input, request(BEACON)).kind, 'beacon-fixture');
assert.equal(localResponse(input, request(RUM, {method: 'POST', body: 'local-csp-probe'})).kind, 'rum-fixture');
for (const url of ['https://unreviewed.invalid/x', 'file:///private', BEACON + '?extra=1', RUM + '/extra']) assert.throws(() => localResponse(input, request(url)));
assert.throws(() => localResponse(input, request(BEACON, {headers: {Cookie: 'fixture'}})), /cookies/);
assert.throws(() => localResponse(input, request(BEACON, {headers: {Authorization: 'fixture'}})), /authorization/);
});

test('promotion fixtures require exact method, event shape and credential absence', (t) => {
const value = fixture(t);
const input = loadInputs(value.root, value.packet, value.inventory);
const valid = request(PROMOTION, {method: 'POST', headers: {'content-type': 'text/plain'}, body: JSON.stringify({source: 'sdk-rust-reference', event: 'impression'})});
assert.equal(localResponse(input, valid).kind, 'promotion-impression');
for (const change of [
{method: 'GET'}, {body: '{invalid'}, {body: JSON.stringify({source: 'other', event: 'click'})},
{body: JSON.stringify({source: 'sdk-rust-reference', event: 'other'})},
{body: JSON.stringify({source: 'sdk-rust-reference', event: 'click', extra: true})},
]) assert.throws(() => localResponse(input, {...valid, ...change}));
});

test('local-origin selection refuses deceptive hosts and added URL components', (t) => {
const value = fixture(t);
const input = loadInputs(value.root, value.packet, value.inventory);
for (const origin of [ORIGIN + '.invalid', ORIGIN + '@unreviewed.invalid', 'https://unreviewed.invalid/' + ORIGIN, ORIGIN + ':444', ORIGIN + '/path', 'http://rust.durable-workflow.com']) {
assert.throws(() => localResponse(input, request(ORIGIN + '/'), origin), /Unexpected local origin/);
}
});

test('fake routing fulfills or aborts without a network continuation', async (t) => {
const value = fixture(t);
const input = loadInputs(value.root, value.packet, value.inventory);
let httpHandler; let websocketHandler;
const context = {route: async (_pattern, handler) => { httpHandler = handler; }, routeWebSocket: async (_pattern, handler) => { websocketHandler = handler; }};
const audit = await installLocalRoutes(context, input);
async function dispatch(url) {
const req = request(url);
const route = {request: () => ({url: () => req.url, method: () => req.method, headers: () => req.headers, postData: () => req.body, resourceType: () => req.resourceType}), fulfill: async () => 'fulfilled', abort: async () => 'aborted'};
await httpHandler(route);
}
await dispatch(ORIGIN + '/');
await dispatch('https://unreviewed.invalid/x');
let closed = false;
websocketHandler({close: () => { closed = true; }});
assert.equal(closed, true);
assert.equal(audit.kinds.artifact, 1);
assert.deepEqual(audit.refused, ['http', 'websocket']);
});

test('review packet identifies the exact executable and data closure', () => {
const packet = JSON.parse(fs.readFileSync(path.join(__dirname, 'rust-local-browser-packet.json')));
assert.equal(packet.run_status, 'not-run');
for (const [name, hash] of Object.entries(packet.files)) assert.equal(digest(fs.readFileSync(path.join(__dirname, name))), hash, name);
const candidate = JSON.parse(fs.readFileSync(path.join(__dirname, 'rust-rule.candidate.json')));
assert.deepEqual(packet.input_artifact, candidate.input_artifact);
assert.equal(packet.container.network, 'none');
});
Loading
Loading