Skip to content

Commit 369c0f5

Browse files
authored
LocalizedStringProvider: nonce parameter for Content Security Policy (#6219)
* nonce parameter for Content Security Policy
1 parent 66e6fab commit 369c0f5

File tree

11 files changed

+281
-6
lines changed

11 files changed

+281
-6
lines changed

examples/next-app-csp/.gitignore

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
.yarn/install-state.gz
8+
9+
# testing
10+
/coverage
11+
12+
# next.js
13+
/.next/
14+
/out/
15+
16+
# production
17+
/build
18+
19+
# misc
20+
.DS_Store
21+
*.pem
22+
23+
# debug
24+
npm-debug.log*
25+
yarn-debug.log*
26+
yarn-error.log*
27+
28+
# local env files
29+
.env*.local
30+
31+
# vercel
32+
.vercel
33+
34+
# typescript
35+
*.tsbuildinfo
36+
next-env.d.ts

examples/next-app-csp/README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2+
3+
## Getting Started
4+
5+
First, run the development server:
6+
7+
```bash
8+
npm run dev
9+
# or
10+
yarn dev
11+
# or
12+
pnpm dev
13+
# or
14+
bun dev
15+
```
16+
17+
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18+
19+
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20+
21+
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
22+
23+
## Learn More
24+
25+
To learn more about Next.js, take a look at the following resources:
26+
27+
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28+
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29+
30+
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
31+
32+
## Deploy on Vercel
33+
34+
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35+
36+
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

examples/next-app-csp/app/layout.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { Metadata } from "next";
2+
import { headers } from "next/headers";
3+
import {
4+
LocalizedStringProvider,
5+
createLocalizedStringDictionary,
6+
} from "@adobe/react-spectrum/i18n";
7+
8+
const dictionary = createLocalizedStringDictionary([
9+
"@react-spectrum/datepicker",
10+
]);
11+
12+
export const metadata: Metadata = {
13+
title: "Create Next App",
14+
description: "Generated by create next app",
15+
};
16+
17+
export default function RootLayout({
18+
children,
19+
}: {
20+
children: React.ReactNode;
21+
}) {
22+
const nonce = headers().get("x-nonce");
23+
console.log("nonce", nonce);
24+
return (
25+
<html lang="en">
26+
<body>
27+
<LocalizedStringProvider
28+
locale="en"
29+
dictionary={dictionary}
30+
nonce={nonce ?? ""}
31+
/>
32+
{children}
33+
</body>
34+
</html>
35+
);
36+
}

examples/next-app-csp/app/page.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"use client";
2+
3+
import {Provider, defaultTheme, DatePicker} from '@adobe/react-spectrum';
4+
import {useRouter} from 'next/navigation';
5+
6+
declare module '@adobe/react-spectrum' {
7+
interface RouterConfig {
8+
routerOptions: NonNullable<Parameters<ReturnType<typeof useRouter>['push']>[1]>
9+
}
10+
}
11+
12+
export default function Home() {
13+
let router = useRouter();
14+
return (
15+
<Provider theme={defaultTheme} locale="en" router={{navigate: router.push}}>
16+
<DatePicker label="Date" />
17+
</Provider>
18+
)
19+
}

examples/next-app-csp/middleware.tsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { NextResponse } from "next/server";
2+
3+
export function middleware(request: Request) {
4+
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
5+
const cspHeader = `
6+
default-src 'self';
7+
script-src 'self' 'nonce-${nonce}' 'strict-dynamic' https: http: 'unsafe-inline' ${
8+
process.env.NODE_ENV === "production" ? "" : `'unsafe-eval'`
9+
};
10+
style-src 'self' 'unsafe-inline';
11+
img-src 'self' blob: data:;
12+
font-src 'self';
13+
object-src 'none';
14+
base-uri 'self';
15+
form-action 'self';
16+
frame-ancestors 'none';
17+
upgrade-insecure-requests;
18+
`;
19+
// Replace newline characters and spaces
20+
const contentSecurityPolicyHeaderValue = cspHeader
21+
.replace(/\s{2,}/g, " ")
22+
.trim();
23+
24+
const requestHeaders = new Headers(request.headers);
25+
requestHeaders.set("x-nonce", nonce);
26+
requestHeaders.set(
27+
"Content-Security-Policy",
28+
contentSecurityPolicyHeaderValue
29+
);
30+
31+
const response = NextResponse.next({
32+
request: {
33+
headers: requestHeaders,
34+
},
35+
});
36+
response.headers.set(
37+
"Content-Security-Policy",
38+
contentSecurityPolicyHeaderValue
39+
);
40+
41+
return response;
42+
}
43+
44+
export const config = {
45+
matcher: [
46+
/*
47+
* Match all request paths except for the ones starting with:
48+
* - api (API routes)
49+
* - _next/static (static files)
50+
* - _next/image (image optimization files)
51+
* - favicon.ico (favicon file)
52+
*/
53+
{
54+
source: "/((?!api|_next/static|_next/image|favicon.ico).*)",
55+
missing: [
56+
{ type: "header", key: "next-router-prefetch" },
57+
{ type: "header", key: "purpose", value: "prefetch" },
58+
],
59+
},
60+
],
61+
};

examples/next-app-csp/next.config.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
const localesPlugin = require('@react-aria/optimize-locales-plugin');
2+
const glob = require('glob');
3+
4+
/** @type {import('next').NextConfig} */
5+
const nextConfig = {
6+
webpack(config, { isServer }) {
7+
if (!isServer) {
8+
// Don't include any locale strings in the client JS bundle.
9+
config.plugins.push(localesPlugin.webpack({ locales: [] }));
10+
}
11+
return config;
12+
},
13+
transpilePackages: [
14+
'@adobe/react-spectrum',
15+
'@react-spectrum/*',
16+
'@spectrum-icons/*',
17+
].flatMap(spec => glob.sync(`${spec}`, { cwd: 'node_modules/' })),
18+
}
19+
20+
module.exports = nextConfig

examples/next-app-csp/package.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"name": "next-app",
3+
"version": "0.1.0",
4+
"private": true,
5+
"scripts": {
6+
"dev": "next dev",
7+
"build": "next build",
8+
"start": "next start",
9+
"lint": "next lint"
10+
},
11+
"dependencies": {
12+
"react": "^18",
13+
"react-dom": "^18",
14+
"next": "14.0.3"
15+
},
16+
"devDependencies": {
17+
"typescript": "^5",
18+
"@types/node": "^20",
19+
"@types/react": "^18",
20+
"@types/react-dom": "^18",
21+
"glob": "^10.3.12"
22+
},
23+
"workspaces": [
24+
"../../packages/react-aria-components",
25+
"../../packages/react-aria",
26+
"../../packages/react-stately",
27+
"../../packages/*/*"
28+
],
29+
"resolutions": {
30+
"react": "link:../../node_modules/react",
31+
"react-dom": "link:../../node_modules/react-dom"
32+
}
33+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"plugins": []
3+
}

examples/next-app-csp/tsconfig.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"compilerOptions": {
3+
"target": "es5",
4+
"lib": ["dom", "dom.iterable", "esnext"],
5+
"allowJs": true,
6+
"skipLibCheck": true,
7+
"strict": true,
8+
"noEmit": true,
9+
"esModuleInterop": true,
10+
"module": "esnext",
11+
"moduleResolution": "bundler",
12+
"resolveJsonModule": true,
13+
"isolatedModules": true,
14+
"jsx": "preserve",
15+
"incremental": true,
16+
"plugins": [
17+
{
18+
"name": "next"
19+
}
20+
],
21+
"paths": {
22+
"@/*": ["./*"]
23+
}
24+
},
25+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26+
"exclude": ["node_modules"]
27+
}

packages/@react-aria/i18n/src/server.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ type PackageLocalizedStrings = {
1919

2020
interface PackageLocalizationProviderProps {
2121
locale: string,
22-
strings: PackageLocalizedStrings
22+
strings: PackageLocalizedStrings,
23+
nonce?: string
2324
}
2425

2526
/**
@@ -32,8 +33,10 @@ export function PackageLocalizationProvider(props: PackageLocalizationProviderPr
3233
return null;
3334
}
3435

35-
let {locale, strings} = props;
36-
return <script dangerouslySetInnerHTML={{__html: getPackageLocalizationScript(locale, strings)}} />;
36+
let {nonce, locale, strings} = props;
37+
// suppressHydrationWarning is necessary because the browser
38+
// remove the nonce parameter from the DOM before hydration
39+
return <script nonce={typeof window === 'undefined' ? nonce : ''} suppressHydrationWarning dangerouslySetInnerHTML={{__html: getPackageLocalizationScript(locale, strings)}} />;
3740
}
3841

3942
/**

0 commit comments

Comments
 (0)