Skip to content

Commit

Permalink
feat: Create nosecone package for creating secure headers (#2237)
Browse files Browse the repository at this point in the history
This implements our `nosecone` package and 2 adapters, Next.js and SvelteKit. These 2 frameworks have some of the best support for nonce-based CSPv3—although Next.js has the caveat of it only working in dynamic mode.

Runtimes like Bun, Deno, and Node.js can use Nosecone directly to set headers on the responses, while adapters are needed for deeper integration. Using middleware works really well for Next.js because we can force the headers to be forwarded and it even detects the nonce from the `script-src` directive, which it adds to each `<script>` tag that webpack generates. For SvelteKit, we need to provide `csp` in the config so it'll add the CSP header, but we also use a hook to add our additional secure headers.

Notably missing:
- Removing X-Powered-By—the frameworks don't give us access to this, we should instead just recommend it be removed.
- A Remix adapter—it needs a lot of logic to thread nonces throughout the application.
- An Express adapter—most people are already using Helmet.
  • Loading branch information
blaine-arcjet authored Nov 19, 2024
1 parent c89aead commit 1e8e73b
Show file tree
Hide file tree
Showing 42 changed files with 4,138 additions and 91 deletions.
3 changes: 3 additions & 0 deletions .github/.release-please-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
"headers": "1.0.0-alpha.28",
"ip": "1.0.0-alpha.28",
"logger": "1.0.0-alpha.28",
"nosecone": "1.0.0-alpha.28",
"nosecone-next": "1.0.0-alpha.28",
"nosecone-sveltekit": "1.0.0-alpha.28",
"protocol": "1.0.0-alpha.28",
"redact": "1.0.0-alpha.28",
"redact-wasm": "1.0.0-alpha.28",
Expand Down
15 changes: 15 additions & 0 deletions .github/release-please-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,18 @@
"component": "@arcjet/logger",
"skip-github-release": true
},
"nosecone": {
"component": "nosecone",
"skip-github-release": true
},
"nosecone-next": {
"component": "@nosecone/next",
"skip-github-release": true
},
"nosecone-sveltekit": {
"component": "@nosecone/sveltekit",
"skip-github-release": true
},
"protocol": {
"component": "@arcjet/protocol",
"skip-github-release": true
Expand Down Expand Up @@ -156,6 +168,9 @@
"@arcjet/ip",
"@arcjet/body",
"@arcjet/logger",
"nosecone",
"@nosecone/next",
"@nosecone/sveltekit",
"@arcjet/protocol",
"@arcjet/redact",
"@arcjet/redact-wasm",
Expand Down
2 changes: 1 addition & 1 deletion examples/nextjs-app-dir-rate-limit/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ const aj = arcjet({
],
});

export default createMiddleware(aj);
export default createMiddleware(aj);
18 changes: 0 additions & 18 deletions examples/nextjs-app-dir-rate-limit/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion examples/nextjs-app-dir-validate-email/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Metadata } from "next";
import { connection } from "next/server";
import { Inter } from "next/font/google";
import "./globals.css";

Expand All @@ -9,11 +10,14 @@ export const metadata: Metadata = {
description: "Generated by create next app",
};

export default function RootLayout({
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
// Opt-out of static generation for every page so the CSP nonce can be applied
await connection()

return (
<html lang="en">
<body className={inter.className}>{children}</body>
Expand Down
8 changes: 8 additions & 0 deletions examples/nextjs-app-dir-validate-email/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createMiddleware } from "@nosecone/next";

export const config = {
// matcher tells Next.js which routes to run the middleware on
matcher: ["/(.*)"],
};

export default createMiddleware();
23 changes: 23 additions & 0 deletions examples/nextjs-app-dir-validate-email/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions examples/nextjs-app-dir-validate-email/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
},
"dependencies": {
"@arcjet/next": "file:../../arcjet-next",
"@nosecone/next": "file:../../nosecone-next",
"next": "15.0.1",
"react": "^18",
"react-dom": "^18"
Expand Down
26 changes: 25 additions & 1 deletion examples/sveltekit/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion examples/sveltekit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"format": "prettier --write ."
},
"dependencies": {
"@arcjet/sveltekit": "file:../../arcjet-sveltekit"
"@arcjet/sveltekit": "file:../../arcjet-sveltekit",
"@nosecone/sveltekit": "file:../../nosecone-sveltekit"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.3.1",
Expand Down
42 changes: 20 additions & 22 deletions examples/sveltekit/src/hooks.server.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,25 @@
import { aj } from "$lib/server/arcjet";
import { error } from "@sveltejs/kit";
import type { RequestEvent } from "@sveltejs/kit";
import { createHook } from "@nosecone/sveltekit";
import { sequence } from "@sveltejs/kit/hooks";

export async function handle({
event,
resolve,
}: {
event: RequestEvent;
resolve: (event: RequestEvent) => Response | Promise<Response>;
}): Promise<Response> {
// Ignore routes that extend the Arcjet rules - they will call `.protect` themselves
const filteredRoutes = ["/api/rate-limited", "/rate-limited"];
if (filteredRoutes.includes(event.url.pathname)) {
// return - route will handle protection
return resolve(event);
}
export const handle = sequence(
createHook(),
async ({ event, resolve }) => {
// Ignore routes that extend the Arcjet rules - they will call `.protect` themselves
const filteredRoutes = ["/api/rate-limited", "/rate-limited"];
if (filteredRoutes.includes(event.url.pathname)) {
// return - route will handle protection
return resolve(event);
}

// Ensure every other route is protected with shield
const decision = await aj.protect(event);
if (decision.isDenied()) {
return error(403, "Forbidden");
}
// Ensure every other route is protected with shield
const decision = await aj.protect(event);
if (decision.isDenied()) {
return error(403, "Forbidden");
}

// Continue with the route
return resolve(event);
}
// Continue with the route
return await resolve(event);
}
)
2 changes: 2 additions & 0 deletions examples/sveltekit/svelte.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import adapter from "@sveltejs/adapter-auto";
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
import { csp } from "@nosecone/sveltekit"

/** @type {import('@sveltejs/kit').Config} */
const config = {
Expand All @@ -8,6 +9,7 @@ const config = {
preprocess: vitePreprocess(),

kit: {
csp: csp(),
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
Expand Down
6 changes: 6 additions & 0 deletions nosecone-next/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/.turbo/
/coverage/
/node_modules/
*.d.ts
*.js
!*.config.js
4 changes: 4 additions & 0 deletions nosecone-next/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: ["@arcjet/eslint-config"],
};
Loading

0 comments on commit 1e8e73b

Please sign in to comment.