mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-10-18 21:57:20 +02:00
Return artifact consent through registered POST callbacks
This commit is contained in:
1 parent
09f94137cd
commit
d3a8b4a8a7
11 files changed
+202
-13
No files matched your search
@@ -0,0 +1,22 @@
|
||||
require('ts-node/register/transpile-only');
|
||||
const { expect } = require('chai');
|
||||
const { createReviewCallbacks } = require('../src/server/service/review-callbacks');
|
||||
const entry = () => ({ clientId:'1'.repeat(32),callbackId:'2'.repeat(32),url:'https://review.example.test/api/v1/artifacts/callback' });
|
||||
const raw = callbacks => JSON.stringify({version:1,callbacks});
|
||||
describe('registered review callback destinations', () => {
|
||||
it('binds exact HTTPS destinations to both client and callback IDs', () => {
|
||||
const registry=createReviewCallbacks(raw([entry()]));
|
||||
expect(registry.resolve(entry().clientId,entry().callbackId)).equal(entry().url);
|
||||
expect(registry.resolve('3'.repeat(32),entry().callbackId)).equal(undefined);
|
||||
expect(registry.resolve(entry().clientId,'4'.repeat(32))).equal(undefined);
|
||||
expect(registry.formAction).equal("'self' "+entry().url);
|
||||
});
|
||||
it('rejects redirect parameters, URL credentials, aliases and unsafe CSP characters', () => {
|
||||
for(const url of ['http://review.example.test/callback','https://owner:secret@review.example.test/callback','https://review.example.test/callback?','https://review.example.test/callback#','https://review.example.test/callback?redirect=https://other.test','https://review.example.test/a/../callback','https://review.example.test/%63allback','https://review.example.test/callback/','https://review.example.test/callback\n','https://review.example.test/','https://review.example.test/callback;script-src']) expect(()=>createReviewCallbacks(raw([{...entry(),url}]))).to.throw('Invalid review callback registry');
|
||||
});
|
||||
it('rejects duplicate decoded configuration fields and bounded registry overflows', () => {
|
||||
for(const value of [raw([]),raw([entry(),entry()]),raw([{...entry(),extra:'x'}]),raw(Array.from({length:33},(_,i)=>({...entry(),callbackId:i.toString(16).padStart(32,'0')}))),raw([entry()]).replace('"version":1','"version":1,"ver\\u0073ion":1'),'{']) expect(()=>createReviewCallbacks(value)).to.throw('Invalid review callback registry');
|
||||
const entries=Array.from({length:4},(_,i)=>({...entry(),callbackId:String(i).repeat(32),url:'https://review.example.test/'+('x'.repeat(1100))+i}));
|
||||
expect(()=>createReviewCallbacks(raw(entries))).to.throw('Invalid review callback registry');
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,9 @@ const { randomBytes } = require("crypto");
|
||||
const {
|
||||
createReviewConsentRouter,
|
||||
} = require("../src/server/service/review-consent-http");
|
||||
const { createReviewCallbacks } = require("../src/server/service/review-callbacks");
|
||||
const callbackURL = "https://review.example.test/api/v1/artifacts/callback";
|
||||
const callbacks = createReviewCallbacks(JSON.stringify({version:1,callbacks:[{clientId:"1".repeat(32),callbackId:"6".repeat(32),url:callbackURL}]}));
|
||||
const origin = "https://anonymous.example.test";
|
||||
const owner = "a".repeat(24);
|
||||
const intent = {
|
||||
@@ -108,6 +111,10 @@ describe("review consent browser HTTP transport", function () {
|
||||
};
|
||||
},
|
||||
};
|
||||
backend.handoff = async (actor, ticket, resolveCallback) => {
|
||||
calls.push({actor,ticket,handoff:true});
|
||||
return { callbackUrl: resolveCallback(intent.clientId,"6".repeat(32)), completion: { contract: intent.contract, clientId: intent.clientId, intentId: intent.intentId, code: "9".repeat(64), expiresAt: new Date(Date.now()+60000).toISOString().replace(/\.\d{3}Z$/, "Z"), privateField: "private-field" }, privateField: "private-field" };
|
||||
};
|
||||
const app = express();
|
||||
// Model a TLS-terminating trusted loopback proxy, never trust arbitrary peers.
|
||||
app.set("trust proxy", "loopback");
|
||||
@@ -134,8 +141,9 @@ describe("review consent browser HTTP transport", function () {
|
||||
});
|
||||
app.use(
|
||||
"/api/review-consent",
|
||||
createReviewConsentRouter(origin, backend, randomBytes(32)),
|
||||
createReviewConsentRouter(origin, backend, randomBytes(32), callbacks),
|
||||
);
|
||||
app.use("/api/review-consent-disabled", createReviewConsentRouter(origin, backend, randomBytes(32)));
|
||||
server = http.createServer(app);
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const login = await request("/test-login");
|
||||
@@ -500,4 +508,27 @@ describe("review consent browser HTTP transport", function () {
|
||||
) + "\n",
|
||||
);
|
||||
});
|
||||
it('offers only a configured, current-session completion POST handoff', async function () {
|
||||
expect((await request('/api/review-consent/csrf')).body.handoffEnabled).equal(true);
|
||||
const response=await request('/api/review-consent/handoff',{body:{ticket:'synthetic-signed-ticket'}});
|
||||
expect(response.status).equal(200);expect(response.body.callbackUrl).equal(callbackURL);
|
||||
expect(response.body.completion.code).equal('9'.repeat(64));
|
||||
expect(Object.keys(response.body.completion).sort()).deep.equal(['clientId','code','contract','expiresAt','intentId']);
|
||||
expect(JSON.stringify(response.body)).not.include('private-field');
|
||||
expect(response.headers['cache-control']).equal('no-store');
|
||||
expect((await request('/api/review-consent/handoff',{body:{ticket:'synthetic-signed-ticket',callbackUrl:'https://other.example.test/callback'}})).status).equal(400);
|
||||
expect((await request('/api/review-consent-disabled/csrf')).body.handoffEnabled).equal(false);
|
||||
expect((await request('/api/review-consent-disabled/handoff',{body:{ticket:'synthetic-signed-ticket'}})).status).equal(404);
|
||||
});
|
||||
it('does not disclose a completion code after concurrent logout', async function () {
|
||||
let entered, release;
|
||||
const pending=new Promise(resolve=>{entered=resolve;});
|
||||
const wait=new Promise(resolve=>{release=resolve;});
|
||||
const original=backend.handoff;
|
||||
backend.handoff=async(...args)=>{entered();await wait;return original(...args);};
|
||||
const response=request('/api/review-consent/handoff',{body:{ticket:'synthetic-signed-ticket'}});
|
||||
await pending;await new Promise(resolve=>store.destroy(sid,resolve));release();
|
||||
const result=await response;expect(result.status).equal(401);expect(JSON.stringify(result.body)).not.include('9'.repeat(64));
|
||||
});
|
||||
|
||||
});
|
||||
@@ -127,4 +127,37 @@ describe('review consent browser page', function () {
|
||||
expect(b.document.body.textContent).not.include('Consent recorded.');
|
||||
});
|
||||
|
||||
it('posts a completion only to the configured callback without placing secrets in URLs or storage', async function () {
|
||||
const completion={contract:'4open.artifacts/1',clientId:'1'.repeat(32),intentId:'2'.repeat(32),code:'9'.repeat(64),expiresAt:new Date(Date.now()+60000).toISOString()};
|
||||
const callbackUrl='https://review.example.test/api/v1/artifacts/callback';
|
||||
b=await browser({'/api/review-consent/csrf':()=>({csrf,handoffEnabled:true}),'/api/review-consent/handoff':()=>({completion,callbackUrl})});
|
||||
const submissions=[];b.w.HTMLFormElement.prototype.submit=function(){submissions.push({action:this.action,method:this.method,completion:JSON.parse(this.elements.namedItem('completion').value)});};
|
||||
await b.preview();await b.accept();await b.click('button[type=button]');
|
||||
expect(b.document.body.textContent).include('Continue to review to finish linking');
|
||||
await b.click('button[type=button]');
|
||||
expect(submissions).deep.equal([{action:callbackUrl,method:'post',completion}]);
|
||||
expect(b.w.location.href).equal('https://anonymous.example.test/review-link');
|
||||
expect(JSON.stringify(b.w.localStorage)).not.include(completion.code);expect(b.w.sessionStorage.length).equal(0);
|
||||
expect(b.document.documentElement.outerHTML).not.include(token).not.include('private-ticket');
|
||||
expect(b.requests.find(r=>r.pathname.endsWith('/handoff')).payload).deep.equal({ticket:'private-ticket'});
|
||||
});
|
||||
it('does not submit a late completion after account change', async function () {
|
||||
let resolve;
|
||||
b=await browser({'/api/review-consent/csrf':()=>({csrf,handoffEnabled:true}),'/api/review-consent/handoff':()=>new Promise(yes=>{resolve=yes;})});
|
||||
const submissions=[];b.w.HTMLFormElement.prototype.submit=function(){submissions.push(this.action);};
|
||||
await b.preview();await b.accept();await b.click('button[type=button]');await b.click('button[type=button]');
|
||||
b.app.state.user={username:'other'};await delay(5);
|
||||
resolve({completion:{contract:'4open.artifacts/1',clientId:'1'.repeat(32),intentId:'2'.repeat(32),code:'9'.repeat(64),expiresAt:new Date(Date.now()+60000).toISOString()},callbackUrl:'https://review.example.test/api/v1/artifacts/callback'});await delay(30);
|
||||
expect(submissions).length(0);expect(b.document.body.textContent).include('Your account changed');
|
||||
expect(b.document.querySelector('#review-repository').value).equal('');
|
||||
});
|
||||
it('rejects a wrong-intent completion or an unsafe callback destination', async function () {
|
||||
let wrongScope=true;
|
||||
b=await browser({'/api/review-consent/csrf':()=>({csrf,handoffEnabled:true}),'/api/review-consent/handoff':()=>({completion:{contract:'4open.artifacts/1',clientId:'1'.repeat(32),intentId:wrongScope?'f'.repeat(32):'2'.repeat(32),code:'9'.repeat(64),expiresAt:new Date(Date.now()+60000).toISOString()},callbackUrl:wrongScope?'https://review.example.test/api/v1/artifacts/callback':'javascript:alert(1)'})});
|
||||
const submissions=[];b.w.HTMLFormElement.prototype.submit=function(){submissions.push(this.action);};
|
||||
await b.preview();await b.accept();await b.click('button[type=button]');await b.click('button[type=button]');
|
||||
expect(submissions).length(0);wrongScope=false;await b.click('button[type=button]');expect(submissions).length(0);
|
||||
expect(b.document.body.textContent).include('return to review could not be prepared');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -783,4 +783,16 @@ describe("review owner consent transactions", function () {
|
||||
expect(await connection.db.collection('review_completions').countDocuments({})).equal(0);
|
||||
});
|
||||
|
||||
it('resolves callback scope from the signed intent before issuing a code', async function () {
|
||||
const quoted=await store.preview(actor(),'synthetic-repository',request());
|
||||
await store.confirm(actor(),quoted.ticket,confirmation());
|
||||
await rejected(store.handoff(actor(),quoted.ticket,()=>undefined),'forbidden');
|
||||
expect(await connection.db.collection('review_completions').countDocuments({})).equal(0);
|
||||
const selected=[];
|
||||
const result=await store.handoff(actor(),quoted.ticket,(client,callback)=>{selected.push([client,callback]);return 'https://review.example.test/api/v1/artifacts/callback';});
|
||||
expect(selected).deep.equal([[intent().clientId,intent().callbackId]]);
|
||||
expect(result.completion).deep.equal(await store.completion(actor(),quoted.ticket));
|
||||
expect(result.callbackUrl).equal('https://review.example.test/api/v1/artifacts/callback');
|
||||
});
|
||||
|
||||
});
|
||||
Reference in new issue
Block a user