Skip to content

Commit de53946

Browse files
authored
Sanitize the back URL used when rendering PDF (#3111)
1 parent d512041 commit de53946

File tree

6 files changed

+92
-160
lines changed

6 files changed

+92
-160
lines changed

.changeset/fair-actors-remain.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"gitbook": patch
3+
---
4+
5+
Fix security issue with injection of "javacript:` url in the back button of PDFs

packages/gitbook/src/components/PDF/PDFPage.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import { TrademarkLink } from '@/components/TableOfContents/Trademark';
2020
import type { PolymorphicComponentProp } from '@/components/utils/types';
2121
import { getSpaceLanguage } from '@/intl/server';
2222
import { tString } from '@/intl/translate';
23-
import { getPagePDFContainerId } from '@/lib/links';
2423
import { resolvePageId } from '@/lib/pages';
2524
import { tcls } from '@/lib/tailwind';
2625
import { defaultCustomization } from '@/lib/utils';
@@ -29,6 +28,7 @@ import { type PDFSearchParams, getPDFSearchParams } from './urls';
2928
import { PageControlButtons } from './PageControlButtons';
3029
import { PrintButton } from './PrintButton';
3130
import './pdf.css';
31+
import { sanitizeGitBookAppURL } from '@/lib/app';
3232

3333
const DEFAULT_LIMIT = 100;
3434

@@ -92,7 +92,10 @@ export async function PDFPage(props: {
9292
<div className={tcls('fixed', 'left-12', 'top-12', 'print:hidden', 'z-50')}>
9393
<a
9494
title={tString(language, 'pdf_goback')}
95-
href={pdfParams.back ?? linker.toAbsoluteURL(linker.toPathInSpace(''))}
95+
href={
96+
(pdfParams.back ? sanitizeGitBookAppURL(pdfParams.back) : null) ??
97+
linker.toAbsoluteURL(linker.toPathInSpace(''))
98+
}
9699
className={tcls(
97100
'flex',
98101
'flex-row',
@@ -353,3 +356,13 @@ function selectPages(
353356
});
354357
return limitTo(allPages);
355358
}
359+
360+
/**
361+
* Create the HTML ID for the container of a page or a given anchor in it.
362+
*/
363+
function getPagePDFContainerId(
364+
page: RevisionPageDocument | RevisionPageGroup,
365+
anchor?: string
366+
): string {
367+
return `pdf-page-${page.id}${anchor ? `-${anchor}` : ''}`;
368+
}

packages/gitbook/src/lib/app.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { GITBOOK_APP_URL } from '@v2/lib/env';
2+
3+
/**
4+
* Create an absolute href in the GitBook application.
5+
*/
6+
export function getGitBookAppHref(pathname: string): string {
7+
const appUrl = new URL(GITBOOK_APP_URL);
8+
appUrl.pathname = pathname;
9+
10+
return appUrl.toString();
11+
}
12+
13+
/**
14+
* Sanitize a URL to be a valid GitBook.com app URL.
15+
*/
16+
export function sanitizeGitBookAppURL(input: string): string | null {
17+
if (!URL.canParse(input)) {
18+
return null;
19+
}
20+
21+
const url = new URL(input);
22+
if (url.origin !== GITBOOK_APP_URL) {
23+
return null;
24+
}
25+
26+
return url.toString();
27+
}

packages/gitbook/src/lib/links.ts

Lines changed: 0 additions & 153 deletions
This file was deleted.

packages/gitbook/src/lib/references.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ import type React from 'react';
1515

1616
import { PageIcon } from '@/components/PageIcon';
1717

18+
import { getGitBookAppHref } from './app';
1819
import { getBlockById, getBlockTitle } from './document';
19-
import { getGitbookAppHref } from './links';
2020
import { resolvePageId } from './pages';
2121
import { findSiteSpaceById } from './sites';
2222
import type { ClassValue } from './tailwind';
@@ -194,7 +194,7 @@ export async function resolveContentRef(
194194

195195
if (!targetSpace) {
196196
return {
197-
href: getGitbookAppHref(`/s/${contentRef.space}`),
197+
href: getGitBookAppHref(`/s/${contentRef.space}`),
198198
text: 'space',
199199
active: false,
200200
};
@@ -224,7 +224,7 @@ export async function resolveContentRef(
224224

225225
case 'collection': {
226226
return {
227-
href: getGitbookAppHref('/home'),
227+
href: getGitBookAppHref('/home'),
228228
text: 'collection',
229229
active: false,
230230
};
@@ -242,7 +242,7 @@ export async function resolveContentRef(
242242
return null;
243243
}
244244
return {
245-
href: getGitbookAppHref(`/s/${space.id}`),
245+
href: getGitBookAppHref(`/s/${space.id}`),
246246
text: reusableContent.title,
247247
active: false,
248248
reusableContent,

packages/gitbook/src/lib/v1.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { createImageResizer } from '@v2/lib/images';
99
import { createLinker } from '@v2/lib/links';
1010

1111
import { DataFetcherError, wrapDataFetcherError } from '@v2/lib/data';
12+
import { headers } from 'next/headers';
1213
import {
1314
type SiteContentPointer,
1415
type SpaceContentPointer,
@@ -31,7 +32,8 @@ import {
3132
searchSiteContent,
3233
} from './api';
3334
import { getDynamicCustomizationSettings } from './customization';
34-
import { getBasePath, getHost, getSiteBasePath } from './links';
35+
import { withLeadingSlash, withTrailingSlash } from './paths';
36+
import { assertIsNotV2 } from './v2';
3537

3638
/*
3739
* Code that will be used until the migration to v2 is complete.
@@ -329,3 +331,41 @@ export function getSitePointerFromContext(context: GitBookSiteContext): SiteCont
329331
siteShareKey: context.shareKey,
330332
};
331333
}
334+
335+
/**
336+
* Return the base path for the current request.
337+
* The value will start and finish with /
338+
*/
339+
async function getBasePath(): Promise<string> {
340+
assertIsNotV2();
341+
const headersList = await headers();
342+
const path = headersList.get('x-gitbook-basepath') ?? '/';
343+
344+
return withTrailingSlash(withLeadingSlash(path));
345+
}
346+
347+
/**
348+
* Return the site base path for the current request.
349+
* The value will start and finish with /
350+
*/
351+
async function getSiteBasePath(): Promise<string> {
352+
assertIsNotV2();
353+
const headersList = await headers();
354+
const path = headersList.get('x-gitbook-site-basepath') ?? '/';
355+
356+
return withTrailingSlash(withLeadingSlash(path));
357+
}
358+
359+
/**
360+
* Return the current host for the current request.
361+
*/
362+
async function getHost(): Promise<string> {
363+
assertIsNotV2();
364+
const headersList = await headers();
365+
const mode = headersList.get('x-gitbook-mode');
366+
if (mode === 'proxy') {
367+
return headersList.get('x-forwarded-host') ?? '';
368+
}
369+
370+
return headersList.get('x-gitbook-host') ?? headersList.get('host') ?? '';
371+
}

0 commit comments

Comments
 (0)