Skip to content

Commit 32f04bb

Browse files
committed
fix(webapp): allow gstatic favicon redirects and changelog images in img-src
1 parent 00bc933 commit 32f04bb

4 files changed

Lines changed: 55 additions & 6 deletions

File tree

.server-changes/org-avatar-csp.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: fix
4+
---
5+
6+
Organization avatars and changelog images now display again in the dashboard instead of showing as broken images.

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

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ function directivePermits(directive: string, imageUrl: string): boolean {
1717
if (!source.startsWith("http")) return false;
1818
const parsed = new URL(source);
1919
if (parsed.protocol !== url.protocol || parsed.host !== url.host) return false;
20-
return parsed.pathname === "/" || parsed.pathname === url.pathname;
20+
// CSP path matching: a source path ending in "/" matches by prefix, otherwise it
21+
// must match exactly. The query string is never part of the match.
22+
return parsed.pathname.endsWith("/")
23+
? url.pathname.startsWith(parsed.pathname)
24+
: parsed.pathname === url.pathname;
2125
});
2226
}
2327

@@ -104,9 +108,9 @@ describe("parseCspImageOrigins", () => {
104108
});
105109

106110
describe("buildImgSrcDirective", () => {
107-
it("is self, data, blob, the SSO avatar hosts and the favicon endpoint by default", () => {
111+
it("is self, data, blob, the SSO avatar hosts, the favicon endpoints and the changelog by default", () => {
108112
expect(buildImgSrcDirective()).toBe(
109-
"img-src 'self' data: blob: https://avatars.githubusercontent.com https://lh3.googleusercontent.com https://www.google.com/s2/favicons"
113+
"img-src 'self' data: blob: https://avatars.githubusercontent.com https://lh3.googleusercontent.com https://www.google.com/s2/favicons https://t0.gstatic.com/faviconV2 https://t1.gstatic.com/faviconV2 https://t2.gstatic.com/faviconV2 https://t3.gstatic.com/faviconV2 https://trigger.dev/changelog/"
110114
);
111115
});
112116

@@ -120,6 +124,29 @@ describe("buildImgSrcDirective", () => {
120124
);
121125
});
122126

127+
it("permits the gstatic shard the favicon endpoint redirects to", () => {
128+
expect(
129+
directivePermits(
130+
buildImgSrcDirective(),
131+
"https://t2.gstatic.com/faviconV2?url=https://example.com&size=128"
132+
)
133+
).toBe(true);
134+
});
135+
136+
it("permits nothing else on a gstatic shard, and no shard we did not list", () => {
137+
const directive = buildImgSrcDirective();
138+
expect(directivePermits(directive, "https://t2.gstatic.com/beacon.png")).toBe(false);
139+
expect(directivePermits(directive, "https://t9.gstatic.com/faviconV2")).toBe(false);
140+
});
141+
142+
it("permits changelog images by path prefix, and nothing else on our domain", () => {
143+
const directive = buildImgSrcDirective();
144+
expect(directivePermits(directive, "https://trigger.dev/changelog/some-post/image.png")).toBe(
145+
true
146+
);
147+
expect(directivePermits(directive, "https://trigger.dev/anything.png")).toBe(false);
148+
});
149+
123150
it("permits both OAuth avatar hosts", () => {
124151
const directive = buildImgSrcDirective();
125152
expect(directivePermits(directive, "https://avatars.githubusercontent.com/u/1?v=4")).toBe(true);

apps/webapp/app/utils/cspImageOrigins.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
*/
77

88
/**
9-
* Always allowed: own origin, inline data, object URLs, the SSO avatar hosts, and the
10-
* favicon endpoint org avatars are stored as (see `utils/favicon.ts`). The path pins
11-
* that one endpoint — CSP matches the path and ignores the query string.
9+
* Always allowed: own origin, inline data, object URLs, the SSO avatar hosts, the
10+
* favicon endpoint org avatars are stored as (see `utils/favicon.ts`), and our own
11+
* changelog images. The path pins each endpoint — CSP matches the path and ignores the
12+
* query string. The favicon endpoint 302s to a `tN.gstatic.com` shard and CSP re-checks
13+
* the redirect target, so the shards are listed too. A trailing "/" matches by prefix.
1214
*/
1315
export const BASE_IMG_SRC_SOURCES = [
1416
"'self'",
@@ -17,6 +19,11 @@ export const BASE_IMG_SRC_SOURCES = [
1719
"https://avatars.githubusercontent.com",
1820
"https://lh3.googleusercontent.com",
1921
"https://www.google.com/s2/favicons",
22+
"https://t0.gstatic.com/faviconV2",
23+
"https://t1.gstatic.com/faviconV2",
24+
"https://t2.gstatic.com/faviconV2",
25+
"https://t3.gstatic.com/faviconV2",
26+
"https://trigger.dev/changelog/",
2027
] as const;
2128

2229
export type RejectedOrigin = { value: string; reason: string };

apps/webapp/test/dashboardAgentImageCsp.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ describe("document image CSP", () => {
2424
expect(directive).not.toMatch(/(^|\s)https?:(\s|$)/);
2525
});
2626

27+
it("permits a changelog image", () => {
28+
const imageUrl = "https://trigger.dev/changelog/some-post/image.png";
29+
const permitted = buildImgSrcDirective()
30+
.split(" ")
31+
.slice(1)
32+
.some((source) => source.endsWith("/") && imageUrl.startsWith(source));
33+
expect(permitted).toBe(true);
34+
});
35+
2736
it("sets the header on every document response, not only on /login", () => {
2837
// The set() call must sit outside the /login branch.
2938
const loginBranch = source.slice(

0 commit comments

Comments
 (0)