fix(api): prevent creation of Response objects outside of request time

This commit is contained in:
41666 2020-12-17 14:23:23 -05:00
parent 9c935f2847
commit 823a99b4eb
5 changed files with 40 additions and 17 deletions

View file

@ -21,6 +21,24 @@ const workerShims = {
let listeners = []; let listeners = [];
let isResponseConstructorAllowed = false;
/**
* SafeResponse wraps a fetch Response to yell loudly if constructed at an unsafe time.
* Cloudflare will reject all Response objects that aren't created during a request, so no pre-generation is allowed.
*/
class SafeResponse extends fetch.Response {
constructor(...args) {
super(...args);
if (!isResponseConstructorAllowed) {
throw new Error(
'Response object created outside of request context. This will be rejected by Cloudflare.'
);
}
}
}
const context = () => const context = () =>
vm.createContext( vm.createContext(
{ {
@ -30,7 +48,7 @@ const context = () =>
listeners.push(fn); listeners.push(fn);
} }
}, },
Response: fetch.Response, Response: SafeResponse,
URL: URL, URL: URL,
crypto: crypto, crypto: crypto,
setTimeout: setTimeout, setTimeout: setTimeout,
@ -75,6 +93,7 @@ const server = http.createServer((req, res) => {
console.log( console.log(
`${loggedStatus} [${timeEnd - timeStart}ms] - ${req.method} ${req.url}` `${loggedStatus} [${timeEnd - timeStart}ms] - ${req.method} ${req.url}`
); );
isResponseConstructorAllowed = false;
}, },
request: new fetch.Request( request: new fetch.Request(
new URL(`http://${req.headers.host || 'localhost'}${req.url}`), new URL(`http://${req.headers.host || 'localhost'}${req.url}`),
@ -95,6 +114,7 @@ const server = http.createServer((req, res) => {
return; return;
} }
isResponseConstructorAllowed = true;
for (let listener of listeners) { for (let listener of listeners) {
try { try {
listener(event); listener(event);

View file

@ -7,14 +7,15 @@ import {
import { respond, withSession } from '../utils/api-tools'; import { respond, withSession } from '../utils/api-tools';
import { getGuild, getGuildData, getGuildMemberRoles } from '../utils/guild'; import { getGuild, getGuildData, getGuildMemberRoles } from '../utils/guild';
const fail = respond( const fail = () =>
respond(
{ {
error: 'guild not found', error: 'guild not found',
}, },
{ {
status: 404, status: 404,
} }
); );
export const GetPickerData = withSession( export const GetPickerData = withSession(
(session?: SessionData) => async (request: Request): Promise<Response> => { (session?: SessionData) => async (request: Request): Promise<Response> => {
@ -38,14 +39,14 @@ export const GetPickerData = withSession(
// Save a Discord API request by checking if this user is a member by session first // Save a Discord API request by checking if this user is a member by session first
const checkGuild = guilds.find((guild) => guild.id === guildID); const checkGuild = guilds.find((guild) => guild.id === guildID);
if (!checkGuild) { if (!checkGuild) {
return fail; return fail();
} }
const guild = await getGuild(guildID, { const guild = await getGuild(guildID, {
skipCachePull: url.searchParams.has('__no_cache'), skipCachePull: url.searchParams.has('__no_cache'),
}); });
if (!guild) { if (!guild) {
return fail; return fail();
} }
const memberRolesP = getGuildMemberRoles({ const memberRolesP = getGuildMemberRoles({
@ -57,7 +58,7 @@ export const GetPickerData = withSession(
const [guildData, memberRoles] = await Promise.all([guildDataP, memberRolesP]); const [guildData, memberRoles] = await Promise.all([guildDataP, memberRolesP]);
if (!memberRoles) { if (!memberRoles) {
return fail; return fail();
} }
const presentableGuild: PresentableGuild = { const presentableGuild: PresentableGuild = {

View file

@ -22,7 +22,7 @@ const AuthErrorResponse = (extra?: string) =>
); );
export const LoginCallback = resolveFailures( export const LoginCallback = resolveFailures(
AuthErrorResponse(), AuthErrorResponse,
async (request: Request): Promise<Response> => { async (request: Request): Promise<Response> => {
const query = new URL(request.url).searchParams; const query = new URL(request.url).searchParams;

View file

@ -34,7 +34,7 @@ export class Router {
this.routingTree[lowerMethod][rootPath] = handler; this.routingTree[lowerMethod][rootPath] = handler;
} }
async handle(request: Request): Promise<Response> | Response { async handle(request: Request): Promise<Response> {
const url = new URL(request.url); const url = new URL(request.url);
if (url.pathname === '/' || url.pathname === '') { if (url.pathname === '/' || url.pathname === '') {

View file

@ -16,14 +16,16 @@ export const respond = (obj: Record<string, any>, init?: ResponseInit) =>
new Response(JSON.stringify(obj), init); new Response(JSON.stringify(obj), init);
export const resolveFailures = ( export const resolveFailures = (
handleWith: Response, handleWith: () => Response,
handler: (request: Request) => Promise<Response> | Response handler: (request: Request) => Promise<Response> | Response
) => async (request: Request): Promise<Response> => { ) => async (request: Request): Promise<Response> => {
try { try {
return handler(request); return handler(request);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
return handleWith || respond({ error: 'internal server error' }, { status: 500 }); return (
handleWith() || respond({ error: 'internal server error' }, { status: 500 })
);
} }
}; };
@ -109,7 +111,7 @@ export const cacheLayer = <Identity, Data>(
const NotAuthenticated = (extra?: string) => const NotAuthenticated = (extra?: string) =>
respond( respond(
{ {
err: extra || 'not authenticated', error: extra || 'not authenticated',
}, },
{ status: 403 } { status: 403 }
); );