Revalidate consent sessions after request body upload

This commit is contained in:
Thomas Durieux committed 2026-10-08 16:24:40 +00:00
1 parent 7b60979c96
commit c24f6d4e94
2 files changed
+69

No files matched your search

+19
View File
@@ -186,6 +186,25 @@ export function createReviewConsentRouter(
}
});
router.use(express.json({ limit: 12288, strict: true, inflate: false }));
// Uploads can outlive the session checked before body parsing. Revalidate
// before any provider call or consent write, not only before the response.
router.use(async (req, res, next) => {
try {
const actor = res.locals.consentActor as Context;
const fresh = await current(req);
if (res.destroyed || res.headersSent || res.locals.consentSignal.aborted)
return;
if (
!fresh ||
fresh.accountId !== actor.accountId ||
fresh.sessionId !== actor.sessionId
)
return reject(res, 401, "unauthorized");
next();
} catch {
reject(res, 503, "unavailable");
}
});
router.post(["/preview", "/confirm"], async (req, res) => {
const actor = res.locals.consentActor as Context;
try {
+50
View File
@@ -3,6 +3,7 @@ const { expect } = require("chai");
const express = require("express");
const session = require("express-session");
const http = require("http");
const { setImmediate } = require("timers");
const { randomBytes } = require("crypto");
const {
createReviewConsentRouter,
@@ -259,6 +260,55 @@ describe("review consent browser HTTP transport", function () {
).to.equal(403);
expect(calls).to.have.length(0);
});
for (const operation of ["preview", "confirm"]) {
for (const change of ["logout", "account replacement"]) {
it(`rejects ${operation} before backend work after ${change} during upload`, async () => {
const original = store.get.bind(store);
let reads = 0, ready;
const authenticated = new Promise((resolve) => { ready = resolve; });
store.get = (id, callback) => original(id, (error, value) => {
callback(error, value);
// The first read restores the session; the second authenticates
// the request before the JSON parser waits for the remaining body.
if (++reads === 2) setImmediate(ready);
});
const body = JSON.stringify(operation === "preview" ? previewInput() : confirmInput());
let upload;
const response = new Promise((resolve, reject) => {
upload = http.request({
host: "127.0.0.1", port: server.address().port,
path: "/api/review-consent/" + operation, method: "POST",
headers: {
"X-Forwarded-Proto": "https", Cookie: cookie, Origin: origin,
"Content-Type": "application/json", "Content-Length": Buffer.byteLength(body),
"X-Review-CSRF": csrf,
},
}, (res) => {
res.resume();
res.on("end", () => resolve(res.statusCode));
});
upload.on("error", reject);
upload.write(body.slice(0, 1));
});
try {
await authenticated;
if (change === "logout") {
await new Promise((resolve) => store.destroy(sid, resolve));
} else {
const saved = await new Promise((resolve) => original(sid, (_error, value) => resolve(value)));
saved.passport.user = "b".repeat(24);
await new Promise((resolve) => store.set(sid, saved, resolve));
}
upload.end(body.slice(1));
expect(await response).to.equal(401);
expect(calls).to.have.length(0);
} finally {
upload.destroy();
store.get = original;
}
});
}
}
it("reloads session authority before returning a pending policy preview", async () => {
let arrived, release;
const ready = new Promise((resolve) => {