Skip to content

Commit f734277

Browse files
committed
fix: domain isolation not being enforced by gql
1 parent 5d0329a commit f734277

File tree

10 files changed

+77
-50
lines changed

10 files changed

+77
-50
lines changed

packages/api/src/config.ts

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const schema = strictObject({
1818
inquiries: string().email(),
1919
uploadLimit: string().transform(parseBytes),
2020
maxPasteLength: number().default(500000),
21+
trustProxy: boolean().default(false),
2122
allowTypes: z
2223
.union([array(string()), string()])
2324
.optional()

packages/api/src/helpers/get-host-from-request.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import type { FastifyRequest } from 'fastify';
1+
import type { FastifyRequest } from "fastify";
22

33
export function getHostFromRequest(request: FastifyRequest): string {
4-
if (request.headers['x-forwarded-host']) {
5-
if (Array.isArray(request.headers['x-forwarded-host'])) {
6-
return request.headers['x-forwarded-host'][0]!;
4+
if (request.headers["x-forwarded-host"]) {
5+
if (Array.isArray(request.headers["x-forwarded-host"])) {
6+
return request.headers["x-forwarded-host"][0]!;
77
}
88

9-
return request.headers['x-forwarded-host'];
9+
return request.headers["x-forwarded-host"];
1010
}
1111

1212
if (request.headers.host) return request.headers.host;
+10-5
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
1-
import type { ExecutionContext } from '@nestjs/common';
2-
import type { GqlContextType } from '@nestjs/graphql';
3-
import { GqlExecutionContext } from '@nestjs/graphql';
4-
import type { FastifyRequest } from 'fastify';
1+
import type { ExecutionContext } from "@nestjs/common";
2+
import type { GqlContextType } from "@nestjs/graphql";
3+
import { GqlExecutionContext } from "@nestjs/graphql";
4+
import type { FastifyRequest } from "fastify";
55

66
export const getRequest = (context: ExecutionContext): FastifyRequest => {
7-
if (context.getType<GqlContextType>() === 'graphql') {
7+
if (context.getType<GqlContextType>() === "graphql") {
88
const ctx = GqlExecutionContext.create(context);
99
return ctx.getContext().req;
1010
}
1111

1212
return context.switchToHttp().getRequest<FastifyRequest>();
1313
};
14+
15+
export const getRequestFromGraphQLContext = (context: any) => {
16+
if ("req" in context) return context.req as FastifyRequest;
17+
throw new Error("Could not get request from context");
18+
};

packages/api/src/helpers/resource.entity-base.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ export abstract class ResourceEntity {
5555
return true;
5656
}
5757

58-
const hostname = getHostFromRequest(request);
5958
if (!config.restrictFilesToHost) return true;
6059

60+
const hostname = getHostFromRequest(request);
6161
// root host can send all files
6262
if (hostname === rootHost.normalised) return true;
6363
if (this.hostname === hostname) return true;

packages/api/src/main.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ await migrate();
1515

1616
const logger = new Logger("bootstrap");
1717
const server = fastify({
18-
trustProxy: process.env.TRUST_PROXY === "true",
1918
maxParamLength: 500,
2019
bodyLimit: config.uploadLimit,
20+
trustProxy: config.trustProxy || process.env.TRUST_PROXY === "true", // legacy
2121
});
2222

2323
const adapter = new FastifyAdapter(server as any);

packages/api/src/modules/file/file.resolver.ts

+16-8
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
import { resolveSelections } from "@jenyus-org/graphql-utils";
2+
import { EntityManager } from "@mikro-orm/core";
23
import { InjectRepository } from "@mikro-orm/nestjs";
34
import { EntityRepository } from "@mikro-orm/postgresql";
4-
import { ForbiddenException, UseGuards } from "@nestjs/common";
5-
import { Args, ID, Info, Mutation, Parent, Query, ResolveField, Resolver } from "@nestjs/graphql";
5+
import { ForbiddenException, NotFoundException, UseGuards } from "@nestjs/common";
6+
import { Args, Context, ID, Info, Mutation, Parent, Query, ResolveField, Resolver } from "@nestjs/graphql";
67
import prettyBytes from "pretty-bytes";
8+
import isValidUtf8 from "utf-8-validate";
9+
import { getRequestFromGraphQLContext } from "../../helpers/get-request.js";
10+
import { isLikelyBinary } from "../../helpers/is-likely-binary.js";
711
import { ResourceLocations } from "../../types/resource-locations.type.js";
812
import { UserId } from "../auth/auth.decorators.js";
913
import { OptionalJWTAuthGuard } from "../auth/guards/optional-jwt.guard.js";
10-
import { FileEntity } from "./file.entity.js";
1114
import { StorageService } from "../storage/storage.service.js";
12-
import isValidUtf8 from "utf-8-validate";
13-
import { isLikelyBinary } from "../../helpers/is-likely-binary.js";
14-
import { EntityManager } from "@mikro-orm/core";
1515
import { ThumbnailEntity } from "../thumbnail/thumbnail.entity.js";
16+
import { FileEntity } from "./file.entity.js";
1617

1718
@Resolver(() => FileEntity)
1819
export class FileResolver {
@@ -26,11 +27,18 @@ export class FileResolver {
2627

2728
@Query(() => FileEntity)
2829
@UseGuards(OptionalJWTAuthGuard)
29-
async file(@Args("fileId", { type: () => ID }) fileId: string, @Info() info: any) {
30+
async file(@Context() context: any, @Args("fileId", { type: () => ID }) fileId: string, @Info() info: any) {
3031
const populate = resolveSelections([{ field: "urls", selector: "owner" }], info) as any[];
31-
return this.fileRepo.findOneOrFail(fileId, {
32+
const file = await this.fileRepo.findOneOrFail(fileId, {
3233
populate,
3334
});
35+
36+
const request = getRequestFromGraphQLContext(context);
37+
if (!file.canSendTo(request)) {
38+
throw new NotFoundException("Your file is in another castle.");
39+
}
40+
41+
return file;
3442
}
3543

3644
@Mutation(() => Boolean)

packages/web/src/@generated/graphql.ts

+16-16
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ export type File = {
7575
urls: ResourceLocations;
7676
};
7777

78+
export type FileEntityPageEdge = {
79+
__typename?: 'FileEntityPageEdge';
80+
cursor: Scalars['String']['output'];
81+
node: File;
82+
};
83+
7884
export type FileMetadata = {
7985
__typename?: 'FileMetadata';
8086
height?: Maybe<Scalars['Float']['output']>;
@@ -83,17 +89,11 @@ export type FileMetadata = {
8389

8490
export type FilePage = {
8591
__typename?: 'FilePage';
86-
edges: Array<FilePageEdge>;
92+
edges: Array<FileEntityPageEdge>;
8793
pageInfo: PageInfo;
8894
totalCount: Scalars['Int']['output'];
8995
};
9096

91-
export type FilePageEdge = {
92-
__typename?: 'FilePageEdge';
93-
cursor: Scalars['String']['output'];
94-
node: File;
95-
};
96-
9797
export type Invite = {
9898
__typename?: 'Invite';
9999
consumed: Scalars['Boolean']['output'];
@@ -214,19 +214,19 @@ export type Paste = {
214214
urls: ResourceLocations;
215215
};
216216

217+
export type PasteEntityPageEdge = {
218+
__typename?: 'PasteEntityPageEdge';
219+
cursor: Scalars['String']['output'];
220+
node: Paste;
221+
};
222+
217223
export type PastePage = {
218224
__typename?: 'PastePage';
219-
edges: Array<PastePageEdge>;
225+
edges: Array<PasteEntityPageEdge>;
220226
pageInfo: PageInfo;
221227
totalCount: Scalars['Int']['output'];
222228
};
223229

224-
export type PastePageEdge = {
225-
__typename?: 'PastePageEdge';
226-
cursor: Scalars['String']['output'];
227-
node: Paste;
228-
};
229-
230230
export type Query = {
231231
__typename?: 'Query';
232232
config: Config;
@@ -323,14 +323,14 @@ export type GetFilesQueryVariables = Exact<{
323323
}>;
324324

325325

326-
export type GetFilesQuery = { __typename?: 'Query', user: { __typename?: 'User', files: { __typename?: 'FilePage', pageInfo: { __typename?: 'PageInfo', endCursor?: string | null, hasNextPage: boolean }, edges: Array<{ __typename?: 'FilePageEdge', node: { __typename?: 'File', id: string, type: string, displayName: string, sizeFormatted: string, thumbnail?: { __typename?: 'Thumbnail', width: number, height: number } | null, paths: { __typename?: 'ResourceLocations', thumbnail?: string | null }, urls: { __typename?: 'ResourceLocations', view: string } } }> } } };
326+
export type GetFilesQuery = { __typename?: 'Query', user: { __typename?: 'User', files: { __typename?: 'FilePage', pageInfo: { __typename?: 'PageInfo', endCursor?: string | null, hasNextPage: boolean }, edges: Array<{ __typename?: 'FileEntityPageEdge', node: { __typename?: 'File', id: string, type: string, displayName: string, sizeFormatted: string, thumbnail?: { __typename?: 'Thumbnail', width: number, height: number } | null, paths: { __typename?: 'ResourceLocations', thumbnail?: string | null }, urls: { __typename?: 'ResourceLocations', view: string } } }> } } };
327327

328328
export type GetPastesQueryVariables = Exact<{
329329
after?: InputMaybe<Scalars['String']['input']>;
330330
}>;
331331

332332

333-
export type GetPastesQuery = { __typename?: 'Query', user: { __typename?: 'User', pastes: { __typename?: 'PastePage', pageInfo: { __typename?: 'PageInfo', endCursor?: string | null, hasNextPage: boolean }, edges: Array<{ __typename?: 'PastePageEdge', node: { __typename?: 'Paste', id: string, title?: string | null, encrypted: boolean, burn: boolean, type: string, createdAt: any, expiresAt?: any | null, urls: { __typename?: 'ResourceLocations', view: string } } }> } } };
333+
export type GetPastesQuery = { __typename?: 'Query', user: { __typename?: 'User', pastes: { __typename?: 'PastePage', pageInfo: { __typename?: 'PageInfo', endCursor?: string | null, hasNextPage: boolean }, edges: Array<{ __typename?: 'PasteEntityPageEdge', node: { __typename?: 'Paste', id: string, title?: string | null, encrypted: boolean, burn: boolean, type: string, createdAt: any, expiresAt?: any | null, urls: { __typename?: 'ResourceLocations', view: string } } }> } } };
334334

335335
export type LoginMutationVariables = Exact<{
336336
username: Scalars['String']['input'];

packages/web/src/renderer/+onRenderHtml.tsx

+18-14
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,33 @@
1-
import { cacheExchange } from '@urql/exchange-graphcache';
2-
import { Provider as UrqlProvider, createClient, fetchExchange, ssrExchange } from '@urql/preact';
3-
import type { HelmetServerState } from 'react-helmet-async';
4-
import { HelmetProvider } from 'react-helmet-async';
5-
import { dangerouslySkipEscape, escapeInject } from 'vike/server';
6-
import type { OnRenderHtmlAsync } from 'vike/types';
7-
import { App } from '../app';
8-
import { cacheOptions } from './cache';
9-
import { renderToStringWithData } from './prepass';
10-
import type { PageProps } from './types';
11-
import { PageContextProvider } from './usePageContext';
1+
import { cacheExchange } from "@urql/exchange-graphcache";
2+
import { Provider as UrqlProvider, createClient, fetchExchange, ssrExchange } from "@urql/preact";
3+
import type { HelmetServerState } from "react-helmet-async";
4+
import { HelmetProvider } from "react-helmet-async";
5+
import { dangerouslySkipEscape, escapeInject } from "vike/server";
6+
import type { OnRenderHtmlAsync } from "vike/types";
7+
import { App } from "../app";
8+
import { cacheOptions } from "./cache";
9+
import { renderToStringWithData } from "./prepass";
10+
import type { PageProps } from "./types";
11+
import { PageContextProvider } from "./usePageContext";
1212

13-
const GRAPHQL_URL = (import.meta.env.PUBLIC_ENV__FRONTEND_API_URL || import.meta.env.FRONTEND_API_URL) + '/graphql';
13+
const GRAPHQL_URL =
14+
(import.meta.env.PUBLIC_ENV__FRONTEND_API_URL || import.meta.env.FRONTEND_API_URL) + "/graphql";
1415

1516
export const onRenderHtml: OnRenderHtmlAsync = async (pageContext): ReturnType<OnRenderHtmlAsync> => {
1617
const { Page } = pageContext;
1718
const pageProps: PageProps = { routeParams: pageContext.routeParams };
1819

19-
const headers = pageContext.cookies ? { Cookie: pageContext.cookies } : undefined;
20+
const headers: Record<string, string> = {};
21+
if (pageContext.cookies) headers.Cookie = pageContext.cookies;
22+
if (pageContext.forwardedHost) headers["x-forwarded-host"] = pageContext.forwardedHost;
23+
2024
const ssr = ssrExchange({ isClient: false });
2125
const client = createClient({
2226
url: GRAPHQL_URL,
2327
exchanges: [ssr, cacheExchange(cacheOptions), fetchExchange],
2428
fetchOptions: {
29+
credentials: "same-origin",
2530
headers: headers,
26-
credentials: 'same-origin',
2731
},
2832
});
2933

packages/web/src/renderer/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ declare global {
99
state?: SSRData;
1010
pageHtml?: string;
1111
cookies?: string;
12+
forwardedHost?: string;
1213
}
1314
}
1415
}

packages/web/src/server/index.ts

+8
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,17 @@ async function startServer() {
8888
cookies = request.headers.cookie;
8989
}
9090

91+
let forwardedFor: string | undefined;
92+
if (request.headers["x-forwarded-for"] && typeof request.headers["x-forwarded-for"] === "string") {
93+
forwardedFor = request.headers["x-forwarded-for"];
94+
} else if (request.headers.host) {
95+
forwardedFor = request.headers.host;
96+
}
97+
9198
const pageContextInit = {
9299
urlOriginal: request.url,
93100
cookies: cookies,
101+
forwardedHost: forwardedFor,
94102
} satisfies Partial<PageContext>;
95103

96104
const pageContext = await renderPage(pageContextInit);

0 commit comments

Comments
 (0)