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 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 = () =>
vm.createContext(
{
@ -30,7 +48,7 @@ const context = () =>
listeners.push(fn);
}
},
Response: fetch.Response,
Response: SafeResponse,
URL: URL,
crypto: crypto,
setTimeout: setTimeout,
@ -75,6 +93,7 @@ const server = http.createServer((req, res) => {
console.log(
`${loggedStatus} [${timeEnd - timeStart}ms] - ${req.method} ${req.url}`
);
isResponseConstructorAllowed = false;
},
request: new fetch.Request(
new URL(`http://${req.headers.host || 'localhost'}${req.url}`),
@ -95,6 +114,7 @@ const server = http.createServer((req, res) => {
return;
}
isResponseConstructorAllowed = true;
for (let listener of listeners) {
try {
listener(event);

View file

@ -7,14 +7,15 @@ import {
import { respond, withSession } from '../utils/api-tools';
import { getGuild, getGuildData, getGuildMemberRoles } from '../utils/guild';
const fail = respond(
{
error: 'guild not found',
},
{
status: 404,
}
);
const fail = () =>
respond(
{
error: 'guild not found',
},
{
status: 404,
}
);
export const GetPickerData = withSession(
(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
const checkGuild = guilds.find((guild) => guild.id === guildID);
if (!checkGuild) {
return fail;
return fail();
}
const guild = await getGuild(guildID, {
skipCachePull: url.searchParams.has('__no_cache'),
});
if (!guild) {
return fail;
return fail();
}
const memberRolesP = getGuildMemberRoles({
@ -57,7 +58,7 @@ export const GetPickerData = withSession(
const [guildData, memberRoles] = await Promise.all([guildDataP, memberRolesP]);
if (!memberRoles) {
return fail;
return fail();
}
const presentableGuild: PresentableGuild = {

View file

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

View file

@ -34,7 +34,7 @@ export class Router {
this.routingTree[lowerMethod][rootPath] = handler;
}
async handle(request: Request): Promise<Response> | Response {
async handle(request: Request): Promise<Response> {
const url = new URL(request.url);
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);
export const resolveFailures = (
handleWith: Response,
handleWith: () => Response,
handler: (request: Request) => Promise<Response> | Response
) => async (request: Request): Promise<Response> => {
try {
return handler(request);
} catch (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) =>
respond(
{
err: extra || 'not authenticated',
error: extra || 'not authenticated',
},
{ status: 403 }
);