Skip to content

Commit cefdc34

Browse files
committed
fix(webapp): reject unsafe hosts when deriving an image policy origin
1 parent c25c7b0 commit cefdc34

3 files changed

Lines changed: 44 additions & 4 deletions

File tree

apps/webapp/app/entry.server.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@ import { env } from "./env.server";
2121
import { eventLoopMonitor, eventLoopUtilizationMonitor } from "./eventLoopMonitor.server";
2222
import { logger } from "./services/logger.server";
2323
import { avatarObjectStoreImageOrigin } from "./services/userAvatar.server";
24-
import { buildImgSrcDirective, parseCspImageOrigins, withImgSrc } from "./utils/cspImageOrigins";
24+
import {
25+
appendImageOrigin,
26+
buildImgSrcDirective,
27+
parseCspImageOrigins,
28+
withImgSrc,
29+
} from "./utils/cspImageOrigins";
2530
import { singleton } from "./utils/singleton";
2631
import { remoteBuildsEnabled } from "./v3/remoteImageBuilder.server";
2732
import {
@@ -82,9 +87,7 @@ const IMG_SRC_DIRECTIVE = buildImgSrcDirective(
8287
);
8388
}
8489

85-
const avatarOrigin = avatarObjectStoreImageOrigin();
86-
87-
return avatarOrigin && !origins.includes(avatarOrigin) ? [...origins, avatarOrigin] : origins;
90+
return appendImageOrigin(origins, avatarObjectStoreImageOrigin());
8891
})
8992
);
9093

apps/webapp/app/utils/cspImageOrigins.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, expect, it } from "vitest";
22
import { faviconUrl } from "./favicon";
33
import {
4+
appendImageOrigin,
45
BASE_IMG_SRC_SOURCES,
56
buildImgSrcDirective,
67
imageOriginFromUrl,
@@ -207,6 +208,10 @@ describe("imageOriginFromUrl", () => {
207208
["empty", ""],
208209
["not a URL", "s3.example.com"],
209210
["a non-http scheme", "s3://bucket"],
211+
["a wildcard host", "http://*.evil.com"],
212+
["a host carrying a directive separator", "http://evil.com;script-src"],
213+
["a host carrying a source separator", "http://evil.com,https://other.test"],
214+
["a host with whitespace", "http://evil.com script-src"],
210215
])("is undefined when the base URL is %s", (_case, value) => {
211216
expect(imageOriginFromUrl(value)).toBeUndefined();
212217
});
@@ -226,3 +231,22 @@ describe("imageOriginFromUrl", () => {
226231
).toBe(false);
227232
});
228233
});
234+
235+
describe("appendImageOrigin", () => {
236+
it("leaves the directive unchanged when no origin is configured", () => {
237+
expect(buildImgSrcDirective(appendImageOrigin([], undefined))).toBe(buildImgSrcDirective());
238+
});
239+
240+
it("does not list an origin twice", () => {
241+
expect(appendImageOrigin(["http://localhost:9005"], "http://localhost:9005")).toEqual([
242+
"http://localhost:9005",
243+
]);
244+
});
245+
246+
it("appends a new origin after the configured ones", () => {
247+
expect(appendImageOrigin(["https://sso.example.com"], "http://localhost:9005")).toEqual([
248+
"https://sso.example.com",
249+
"http://localhost:9005",
250+
]);
251+
});
252+
});

apps/webapp/app/utils/cspImageOrigins.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ function rejectionReason(value: string, allowHttp: boolean): string | undefined
120120
export function imageOriginFromUrl(baseUrl: string | undefined | null): string | undefined {
121121
if (!baseUrl) return undefined;
122122

123+
// `new URL` keeps these in the host, and a ";" or "," would truncate or inject a
124+
// directive once the sources are space-joined.
125+
if (/[*;,]|\s/.test(baseUrl)) return undefined;
126+
123127
let url: URL;
124128
try {
125129
url = new URL(baseUrl);
@@ -134,6 +138,15 @@ export function imageOriginFromUrl(baseUrl: string | undefined | null): string |
134138
return `${url.protocol}//${url.host}`;
135139
}
136140

141+
/** Adds an optional origin to a source list, keeping it free of duplicates. */
142+
export function appendImageOrigin(
143+
origins: readonly string[],
144+
origin: string | undefined
145+
): string[] {
146+
if (!origin || origins.includes(origin)) return [...origins];
147+
return [...origins, origin];
148+
}
149+
137150
/** The full directive: the base sources plus any configured extra origins. */
138151
export function buildImgSrcDirective(extraOrigins: readonly string[] = []): string {
139152
return ["img-src", ...BASE_IMG_SRC_SOURCES, ...extraOrigins].join(" ");

0 commit comments

Comments
 (0)