From f734277c3347bf85bac6dbbca4fe4135cca02eca Mon Sep 17 00:00:00 2001 From: Sylver Date: Mon, 20 May 2024 11:45:55 +0800 Subject: [PATCH] fix: domain isolation not being enforced by gql --- packages/api/src/config.ts | 1 + .../api/src/helpers/get-host-from-request.ts | 10 +++--- packages/api/src/helpers/get-request.ts | 15 ++++++--- .../api/src/helpers/resource.entity-base.ts | 2 +- packages/api/src/main.ts | 2 +- .../api/src/modules/file/file.resolver.ts | 24 +++++++++----- packages/web/src/@generated/graphql.ts | 32 +++++++++---------- packages/web/src/renderer/+onRenderHtml.tsx | 32 +++++++++++-------- packages/web/src/renderer/types.ts | 1 + packages/web/src/server/index.ts | 8 +++++ 10 files changed, 77 insertions(+), 50 deletions(-) diff --git a/packages/api/src/config.ts b/packages/api/src/config.ts index f87aa37..07d63fd 100644 --- a/packages/api/src/config.ts +++ b/packages/api/src/config.ts @@ -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() diff --git a/packages/api/src/helpers/get-host-from-request.ts b/packages/api/src/helpers/get-host-from-request.ts index ec3b6d5..45b14cb 100644 --- a/packages/api/src/helpers/get-host-from-request.ts +++ b/packages/api/src/helpers/get-host-from-request.ts @@ -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; diff --git a/packages/api/src/helpers/get-request.ts b/packages/api/src/helpers/get-request.ts index 8f2c61e..2309d6a 100644 --- a/packages/api/src/helpers/get-request.ts +++ b/packages/api/src/helpers/get-request.ts @@ -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() === 'graphql') { + if (context.getType() === "graphql") { const ctx = GqlExecutionContext.create(context); return ctx.getContext().req; } return context.switchToHttp().getRequest(); }; + +export const getRequestFromGraphQLContext = (context: any) => { + if ("req" in context) return context.req as FastifyRequest; + throw new Error("Could not get request from context"); +}; diff --git a/packages/api/src/helpers/resource.entity-base.ts b/packages/api/src/helpers/resource.entity-base.ts index 311b681..fd2a54d 100644 --- a/packages/api/src/helpers/resource.entity-base.ts +++ b/packages/api/src/helpers/resource.entity-base.ts @@ -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; diff --git a/packages/api/src/main.ts b/packages/api/src/main.ts index bcc7100..0326251 100644 --- a/packages/api/src/main.ts +++ b/packages/api/src/main.ts @@ -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); diff --git a/packages/api/src/modules/file/file.resolver.ts b/packages/api/src/modules/file/file.resolver.ts index 0a1280b..ce6dd7f 100644 --- a/packages/api/src/modules/file/file.resolver.ts +++ b/packages/api/src/modules/file/file.resolver.ts @@ -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 { @@ -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) diff --git a/packages/web/src/@generated/graphql.ts b/packages/web/src/@generated/graphql.ts index bf99360..1081d5b 100644 --- a/packages/web/src/@generated/graphql.ts +++ b/packages/web/src/@generated/graphql.ts @@ -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; @@ -83,17 +89,11 @@ export type FileMetadata = { export type FilePage = { __typename?: 'FilePage'; - edges: Array; + edges: Array; 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']; @@ -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; + edges: Array; pageInfo: PageInfo; totalCount: Scalars['Int']['output']; }; -export type PastePageEdge = { - __typename?: 'PastePageEdge'; - cursor: Scalars['String']['output']; - node: Paste; -}; - export type Query = { __typename?: 'Query'; config: Config; @@ -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; }>; -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']; diff --git a/packages/web/src/renderer/+onRenderHtml.tsx b/packages/web/src/renderer/+onRenderHtml.tsx index e91ec7f..8790007 100644 --- a/packages/web/src/renderer/+onRenderHtml.tsx +++ b/packages/web/src/renderer/+onRenderHtml.tsx @@ -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 => { const { Page } = pageContext; const pageProps: PageProps = { routeParams: pageContext.routeParams }; - const headers = pageContext.cookies ? { Cookie: pageContext.cookies } : undefined; + const headers: Record = {}; + 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', }, }); diff --git a/packages/web/src/renderer/types.ts b/packages/web/src/renderer/types.ts index 399f9d5..d98ce06 100644 --- a/packages/web/src/renderer/types.ts +++ b/packages/web/src/renderer/types.ts @@ -9,6 +9,7 @@ declare global { state?: SSRData; pageHtml?: string; cookies?: string; + forwardedHost?: string; } } } diff --git a/packages/web/src/server/index.ts b/packages/web/src/server/index.ts index 6d8c3ef..186235f 100644 --- a/packages/web/src/server/index.ts +++ b/packages/web/src/server/index.ts @@ -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; const pageContext = await renderPage(pageContextInit);