Skip to content

Commit

Permalink
fix: domain isolation not being enforced by gql
Browse files Browse the repository at this point in the history
  • Loading branch information
sylv committed May 20, 2024
1 parent 5d0329a commit f734277
Show file tree
Hide file tree
Showing 10 changed files with 77 additions and 50 deletions.
1 change: 1 addition & 0 deletions packages/api/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const schema = strictObject({
inquiries: string().email(),
uploadLimit: string().transform(parseBytes),
maxPasteLength: number().default(500000),
trustProxy: boolean().default(false),
allowTypes: z
.union([array(string()), string()])
.optional()
Expand Down
10 changes: 5 additions & 5 deletions packages/api/src/helpers/get-host-from-request.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { FastifyRequest } from 'fastify';
import type { FastifyRequest } from "fastify";

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

return request.headers['x-forwarded-host'];
return request.headers["x-forwarded-host"];
}

if (request.headers.host) return request.headers.host;
Expand Down
15 changes: 10 additions & 5 deletions packages/api/src/helpers/get-request.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import type { ExecutionContext } from '@nestjs/common';
import type { GqlContextType } from '@nestjs/graphql';
import { GqlExecutionContext } from '@nestjs/graphql';
import type { FastifyRequest } from 'fastify';
import type { ExecutionContext } from "@nestjs/common";
import type { GqlContextType } from "@nestjs/graphql";
import { GqlExecutionContext } from "@nestjs/graphql";
import type { FastifyRequest } from "fastify";

export const getRequest = (context: ExecutionContext): FastifyRequest => {
if (context.getType<GqlContextType>() === 'graphql') {
if (context.getType<GqlContextType>() === "graphql") {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req;
}

return context.switchToHttp().getRequest<FastifyRequest>();
};

export const getRequestFromGraphQLContext = (context: any) => {
if ("req" in context) return context.req as FastifyRequest;
throw new Error("Could not get request from context");
};
2 changes: 1 addition & 1 deletion packages/api/src/helpers/resource.entity-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ export abstract class ResourceEntity {
return true;
}

const hostname = getHostFromRequest(request);
if (!config.restrictFilesToHost) return true;

const hostname = getHostFromRequest(request);
// root host can send all files
if (hostname === rootHost.normalised) return true;
if (this.hostname === hostname) return true;
Expand Down
2 changes: 1 addition & 1 deletion packages/api/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ await migrate();

const logger = new Logger("bootstrap");
const server = fastify({
trustProxy: process.env.TRUST_PROXY === "true",
maxParamLength: 500,
bodyLimit: config.uploadLimit,
trustProxy: config.trustProxy || process.env.TRUST_PROXY === "true", // legacy
});

const adapter = new FastifyAdapter(server as any);
Expand Down
24 changes: 16 additions & 8 deletions packages/api/src/modules/file/file.resolver.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { resolveSelections } from "@jenyus-org/graphql-utils";
import { EntityManager } from "@mikro-orm/core";
import { InjectRepository } from "@mikro-orm/nestjs";
import { EntityRepository } from "@mikro-orm/postgresql";
import { ForbiddenException, UseGuards } from "@nestjs/common";
import { Args, ID, Info, Mutation, Parent, Query, ResolveField, Resolver } from "@nestjs/graphql";
import { ForbiddenException, NotFoundException, UseGuards } from "@nestjs/common";
import { Args, Context, ID, Info, Mutation, Parent, Query, ResolveField, Resolver } from "@nestjs/graphql";
import prettyBytes from "pretty-bytes";
import isValidUtf8 from "utf-8-validate";
import { getRequestFromGraphQLContext } from "../../helpers/get-request.js";
import { isLikelyBinary } from "../../helpers/is-likely-binary.js";
import { ResourceLocations } from "../../types/resource-locations.type.js";
import { UserId } from "../auth/auth.decorators.js";
import { OptionalJWTAuthGuard } from "../auth/guards/optional-jwt.guard.js";
import { FileEntity } from "./file.entity.js";
import { StorageService } from "../storage/storage.service.js";
import isValidUtf8 from "utf-8-validate";
import { isLikelyBinary } from "../../helpers/is-likely-binary.js";
import { EntityManager } from "@mikro-orm/core";
import { ThumbnailEntity } from "../thumbnail/thumbnail.entity.js";
import { FileEntity } from "./file.entity.js";

@Resolver(() => FileEntity)
export class FileResolver {
Expand All @@ -26,11 +27,18 @@ export class FileResolver {

@Query(() => FileEntity)
@UseGuards(OptionalJWTAuthGuard)
async file(@Args("fileId", { type: () => ID }) fileId: string, @Info() info: any) {
async file(@Context() context: any, @Args("fileId", { type: () => ID }) fileId: string, @Info() info: any) {
const populate = resolveSelections([{ field: "urls", selector: "owner" }], info) as any[];
return this.fileRepo.findOneOrFail(fileId, {
const file = await this.fileRepo.findOneOrFail(fileId, {
populate,
});

const request = getRequestFromGraphQLContext(context);
if (!file.canSendTo(request)) {
throw new NotFoundException("Your file is in another castle.");
}

return file;
}

@Mutation(() => Boolean)
Expand Down
32 changes: 16 additions & 16 deletions packages/web/src/@generated/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ export type File = {
urls: ResourceLocations;
};

export type FileEntityPageEdge = {
__typename?: 'FileEntityPageEdge';
cursor: Scalars['String']['output'];
node: File;
};

export type FileMetadata = {
__typename?: 'FileMetadata';
height?: Maybe<Scalars['Float']['output']>;
Expand All @@ -83,17 +89,11 @@ export type FileMetadata = {

export type FilePage = {
__typename?: 'FilePage';
edges: Array<FilePageEdge>;
edges: Array<FileEntityPageEdge>;
pageInfo: PageInfo;
totalCount: Scalars['Int']['output'];
};

export type FilePageEdge = {
__typename?: 'FilePageEdge';
cursor: Scalars['String']['output'];
node: File;
};

export type Invite = {
__typename?: 'Invite';
consumed: Scalars['Boolean']['output'];
Expand Down Expand Up @@ -214,19 +214,19 @@ export type Paste = {
urls: ResourceLocations;
};

export type PasteEntityPageEdge = {
__typename?: 'PasteEntityPageEdge';
cursor: Scalars['String']['output'];
node: Paste;
};

export type PastePage = {
__typename?: 'PastePage';
edges: Array<PastePageEdge>;
edges: Array<PasteEntityPageEdge>;
pageInfo: PageInfo;
totalCount: Scalars['Int']['output'];
};

export type PastePageEdge = {
__typename?: 'PastePageEdge';
cursor: Scalars['String']['output'];
node: Paste;
};

export type Query = {
__typename?: 'Query';
config: Config;
Expand Down Expand Up @@ -323,14 +323,14 @@ export type GetFilesQueryVariables = Exact<{
}>;


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 } } }> } } };
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 } } }> } } };

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


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 } } }> } } };
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 } } }> } } };

export type LoginMutationVariables = Exact<{
username: Scalars['String']['input'];
Expand Down
32 changes: 18 additions & 14 deletions packages/web/src/renderer/+onRenderHtml.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,33 @@
import { cacheExchange } from '@urql/exchange-graphcache';
import { Provider as UrqlProvider, createClient, fetchExchange, ssrExchange } from '@urql/preact';
import type { HelmetServerState } from 'react-helmet-async';
import { HelmetProvider } from 'react-helmet-async';
import { dangerouslySkipEscape, escapeInject } from 'vike/server';
import type { OnRenderHtmlAsync } from 'vike/types';
import { App } from '../app';
import { cacheOptions } from './cache';
import { renderToStringWithData } from './prepass';
import type { PageProps } from './types';
import { PageContextProvider } from './usePageContext';
import { cacheExchange } from "@urql/exchange-graphcache";
import { Provider as UrqlProvider, createClient, fetchExchange, ssrExchange } from "@urql/preact";
import type { HelmetServerState } from "react-helmet-async";
import { HelmetProvider } from "react-helmet-async";
import { dangerouslySkipEscape, escapeInject } from "vike/server";
import type { OnRenderHtmlAsync } from "vike/types";
import { App } from "../app";
import { cacheOptions } from "./cache";
import { renderToStringWithData } from "./prepass";
import type { PageProps } from "./types";
import { PageContextProvider } from "./usePageContext";

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

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

const headers = pageContext.cookies ? { Cookie: pageContext.cookies } : undefined;
const headers: Record<string, string> = {};
if (pageContext.cookies) headers.Cookie = pageContext.cookies;
if (pageContext.forwardedHost) headers["x-forwarded-host"] = pageContext.forwardedHost;

const ssr = ssrExchange({ isClient: false });
const client = createClient({
url: GRAPHQL_URL,
exchanges: [ssr, cacheExchange(cacheOptions), fetchExchange],
fetchOptions: {
credentials: "same-origin",
headers: headers,
credentials: 'same-origin',
},
});

Expand Down
1 change: 1 addition & 0 deletions packages/web/src/renderer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ declare global {
state?: SSRData;
pageHtml?: string;
cookies?: string;
forwardedHost?: string;
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions packages/web/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,17 @@ async function startServer() {
cookies = request.headers.cookie;
}

let forwardedFor: string | undefined;
if (request.headers["x-forwarded-for"] && typeof request.headers["x-forwarded-for"] === "string") {
forwardedFor = request.headers["x-forwarded-for"];
} else if (request.headers.host) {
forwardedFor = request.headers.host;
}

const pageContextInit = {
urlOriginal: request.url,
cookies: cookies,
forwardedHost: forwardedFor,
} satisfies Partial<PageContext>;

const pageContext = await renderPage(pageContextInit);
Expand Down

0 comments on commit f734277

Please sign in to comment.