Skip to content

Commit 3e1efdd

Browse files
committed
fix: add more trusted origins to CSP
1 parent fc77d7f commit 3e1efdd

File tree

3 files changed

+121
-1
lines changed

3 files changed

+121
-1
lines changed

modules/security-headers.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { defineNuxtModule } from 'nuxt/kit'
2+
import { BLUESKY_API } from '#shared/utils/constants'
23
import { ALL_KNOWN_GIT_API_ORIGINS } from '#shared/utils/git-providers'
34
import { TRUSTED_IMAGE_DOMAINS } from '#server/utils/image-proxy'
45

@@ -19,10 +20,13 @@ import { TRUSTED_IMAGE_DOMAINS } from '#server/utils/image-proxy'
1920
export default defineNuxtModule({
2021
meta: { name: 'security-headers' },
2122
setup(_, nuxt) {
23+
// These assets are embedded directly on blog pages and should not affect image-proxy trust.
24+
const cspOnlyImgOrigins = ['https://api.star-history.com', 'https://cdn.bsky.app']
2225
const imgSrc = [
2326
"'self'",
2427
'data:',
2528
...TRUSTED_IMAGE_DOMAINS.map(domain => `https://${domain}`),
29+
...cspOnlyImgOrigins,
2630
].join(' ')
2731

2832
const connectSrc = [
@@ -31,6 +35,7 @@ export default defineNuxtModule({
3135
'https://registry.npmjs.org',
3236
'https://api.npmjs.org',
3337
'https://npm.antfu.dev',
38+
BLUESKY_API,
3439
...ALL_KNOWN_GIT_API_ORIGINS,
3540
// Local CLI connector (npmx CLI communicates via localhost)
3641
'http://127.0.0.1:*',

test/e2e/security-headers.spec.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ test.describe('security headers', () => {
1010
.locator('meta[http-equiv="Content-Security-Policy"]')
1111
.getAttribute('content')
1212
expect(cspContent).toContain("script-src 'self'")
13+
expect(cspContent).toContain('https://api.star-history.com')
14+
expect(cspContent).toContain('https://cdn.bsky.app')
15+
expect(cspContent).toContain('https://public.api.bsky.app')
1316

1417
// Other security headers via route rules
1518
expect(headers['x-content-type-options']).toBe('nosniff')
@@ -25,7 +28,7 @@ test.describe('security headers', () => {
2528

2629
// Navigate key pages and assert no CSP violations are logged.
2730
// This catches new external resources that weren't added to the CSP.
28-
const PAGES = ['/', '/package/nuxt', '/search?q=vue', '/compare'] as const
31+
const PAGES = ['/', '/package/nuxt', '/search?q=vue', '/compare', '/blog/alpha-release'] as const
2932

3033
for (const path of PAGES) {
3134
test(`no CSP violations on ${path}`, async ({ goto, cspViolations }) => {

test/fixtures/mock-routes.cjs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,111 @@ function matchConstellationApi(urlString) {
467467
return json(null)
468468
}
469469

470+
const BLUESKY_EMBED_DID = 'did:plc:2gkh62xvzokhlf6li4ol3b3d'
471+
472+
/**
473+
* @param {string} urlString
474+
* @returns {MockResponse | null}
475+
*/
476+
function matchBlueskyApi(urlString) {
477+
const url = new URL(urlString)
478+
479+
if (url.pathname === '/xrpc/com.atproto.identity.resolveHandle') {
480+
return json({ did: BLUESKY_EMBED_DID })
481+
}
482+
483+
if (url.pathname === '/xrpc/app.bsky.feed.getPosts') {
484+
const requestedUri =
485+
url.searchParams.getAll('uris')[0] ||
486+
`at://${BLUESKY_EMBED_DID}/app.bsky.feed.post/3md3cmrg56k2r`
487+
488+
return json({
489+
posts: [
490+
{
491+
uri: requestedUri,
492+
author: {
493+
did: BLUESKY_EMBED_DID,
494+
handle: 'danielroe.dev',
495+
displayName: 'Daniel Roe',
496+
avatar: `https://cdn.bsky.app/img/avatar/plain/${BLUESKY_EMBED_DID}/mock-avatar@jpeg`,
497+
},
498+
record: {
499+
text: 'Mock Bluesky post for CSP coverage.',
500+
createdAt: '2026-03-03T12:00:00.000Z',
501+
},
502+
embed: {
503+
$type: 'app.bsky.embed.images#view',
504+
images: [
505+
{
506+
thumb: `https://cdn.bsky.app/img/feed_thumbnail/plain/${BLUESKY_EMBED_DID}/mock-image@jpeg`,
507+
fullsize: `https://cdn.bsky.app/img/feed_fullsize/plain/${BLUESKY_EMBED_DID}/mock-image@jpeg`,
508+
alt: 'Mock Bluesky image',
509+
aspectRatio: { width: 1200, height: 630 },
510+
},
511+
],
512+
},
513+
likeCount: 42,
514+
replyCount: 7,
515+
repostCount: 3,
516+
},
517+
],
518+
})
519+
}
520+
521+
if (url.pathname === '/xrpc/app.bsky.actor.getProfiles') {
522+
const actors = url.searchParams.getAll('actors')
523+
524+
return json({
525+
profiles: actors.map(handle => ({
526+
handle,
527+
avatar: `https://cdn.bsky.app/img/avatar/plain/${BLUESKY_EMBED_DID}/mock-avatar`,
528+
})),
529+
})
530+
}
531+
532+
return null
533+
}
534+
535+
/**
536+
* @param {string} _urlString
537+
* @returns {MockResponse}
538+
*/
539+
function matchBlueskyCdn(_urlString) {
540+
return {
541+
status: 200,
542+
contentType: 'image/svg+xml',
543+
body:
544+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120">' +
545+
'<rect width="120" height="120" fill="#0ea5e9"/>' +
546+
'<circle cx="60" cy="44" r="18" fill="#f8fafc"/>' +
547+
'<rect x="24" y="74" width="72" height="18" rx="9" fill="#f8fafc"/>' +
548+
'</svg>',
549+
}
550+
}
551+
552+
/**
553+
* @param {string} urlString
554+
* @returns {MockResponse | null}
555+
*/
556+
function matchStarHistoryApi(urlString) {
557+
const url = new URL(urlString)
558+
559+
if (url.pathname === '/svg') {
560+
return {
561+
status: 200,
562+
contentType: 'image/svg+xml',
563+
body:
564+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 320">' +
565+
'<rect width="640" height="320" fill="#0f172a"/>' +
566+
'<path d="M32 256 L160 220 L288 168 L416 120 L544 88 L608 64" stroke="#f59e0b" stroke-width="8" fill="none"/>' +
567+
'<text x="32" y="44" fill="#f8fafc" font-family="monospace" font-size="24">Mock Star History</text>' +
568+
'</svg>',
569+
}
570+
}
571+
572+
return null
573+
}
574+
470575
/**
471576
* @param {string} urlString
472577
* @returns {MockResponse | null}
@@ -524,6 +629,13 @@ const routes = [
524629
pattern: 'https://constellation.microcosm.blue/**',
525630
match: matchConstellationApi,
526631
},
632+
{ name: 'Bluesky API', pattern: 'https://public.api.bsky.app/**', match: matchBlueskyApi },
633+
{ name: 'Bluesky CDN', pattern: 'https://cdn.bsky.app/**', match: matchBlueskyCdn },
634+
{
635+
name: 'Star History API',
636+
pattern: 'https://api.star-history.com/**',
637+
match: matchStarHistoryApi,
638+
},
527639
]
528640

529641
/**

0 commit comments

Comments
 (0)