Arcjet Nosecone: security headers for JS frameworks
Arcjet Nosecone is an open source library
that helps set security headers such as Content-Security-Policy (CSP),
Strict-Transport-Security (HSTS), and X-Content-Type-Options in JS
applications built with Bun, Deno, Next.js, Node.js, or SvelteKit.
What are Arcjet utilities?
Arcjet utilities are independent libraries that do not require the use of the main Arcjet SDK - they can be used with or without other Arcjet rules.
We take the pain out of implementing security tasks through these utilities to provide a security as code approach to developer-first security.
Supported headers
Section titled “Supported headers”Nosecone helps you add and configure these headers:
Content-Security-Policy(CSP)Cross-Origin-Embedder-Policy(COEP)Cross-Origin-Opener-PolicyCross-Origin-Resource-PolicyOrigin-Agent-ClusterReferrer-PolicyStrict-Transport-Security(HSTS)X-Content-Type-OptionsX-DNS-Prefetch-ControlX-Download-OptionsX-Frame-OptionsX-Permitted-Cross-Domain-PoliciesX-XSS-Protection
For full details on each option, see the reference guide.
Quick start
Section titled “Quick start”This guide shows you how to add our recommended
1. Install Nosecone
Section titled “1. Install Nosecone”In your project root, install the Arcjet Nosecone library for your framework:
bun add noseconedeno add npm:noseconenpm i @nosecone/nextpnpm add @nosecone/nextyarn add @nosecone/nextnpm i noseconepnpm add noseconeyarn add noseconenpm i @nosecone/sveltekitpnpm add @nosecone/sveltekityarn add @nosecone/sveltekit2. Configure your application
Section titled “2. Configure your application”Nosecone creates headers that can be used directly as the Response headers in
your Deno server.
import nosecone from "npm:nosecone";
Deno.serve({ port: 3000 }, async (req) => { return new Response("Hello world", { headers: nosecone(), });});Nosecone creates headers that can be used directly as the Response headers in
your Bun server.
import nosecone from "nosecone";
Bun.serve({ port: 3000, async fetch(req: Request) { return new Response("Hello world", { headers: nosecone(), }); },});import nosecone from "nosecone";
Bun.serve({ port: 3000, async fetch(req) { return new Response("Hello world", { headers: nosecone(), }); },});Nosecone applies headers to all your routes with middleware in your Next.js application.
import { createMiddleware } from "@nosecone/next";
// Remove your middleware matcher so Nosecone runs on every route.
export default createMiddleware();import { createMiddleware } from "@nosecone/next";
// Remove your middleware matcher so Nosecone runs on every route.
export default createMiddleware();We recommend you remove any export const config = ... from your middleware so
Nosecone runs on every route. This ensures the headers are applied to all
requests.
You also need to opt-out of static generation in your application. See the
@nosecone/next reference guide for more details
about this requirement.
To opt-out of static generation, modify your layout file with the following:
import { connection } from "next/server";import type { PropsWithChildren } from "react";
export default async function RootLayout(props: PropsWithChildren) { // Opt-out of static generation for every page so the CSP nonce can be applied await connection();
// ...}import { connection } from "next/server";
export default async function RootLayout(props) { // Opt-out of static generation for every page so the CSP nonce can be applied await connection();
// ...}We recommend you remove any export const config = ... from your middleware so
Nosecone runs on every route. This ensures the headers are applied to all
requests.
Nosecone applies headers to all your routes with hooks in your SvelteKit application.
Create the src/hooks.server.ts file in your project with the contents:
import { createHook } from "@nosecone/sveltekit";import { sequence } from "@sveltejs/kit/hooks";
export const handle = sequence(createHook());Create the src/hooks.server.js file in your project with the contents:
import { createHook } from "@nosecone/sveltekit";import { sequence } from "@sveltejs/kit/hooks";
export const handle = sequence(createHook());SvelteKit provides the Content-Security-Policy header itself, so Nosecone
helps you to configure it.
Update your svelte.config.js to configure csp:
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 = { preprocess: vitePreprocess(),
kit: { // Apply CSP with Nosecone defaults csp: csp(), adapter: adapter(), },};
export default config;Nosecone creates headers that can be applied directly with res.setHeaders() in
your Node.js server.
import nosecone from "nosecone";import * as http from "node:http";
const server = http.createServer(async function (req, res) { res.setHeaders(nosecone()); res.writeHead(200, { "Content-Type": "text/plain" }); res.end("Hello world");});
server.listen(3000);import nosecone from "nosecone";import * as http from "node:http";
const server = http.createServer(async function ( req: http.IncomingMessage, res: http.ServerResponse,) { res.setHeaders(nosecone()); res.writeHead(200, { "Content-Type": "text/plain" }); res.end("Hello world");});
server.listen(3000);3. Run your application
Section titled “3. Run your application”bun run --hot index.tsbun run --hot index.jsdeno run --watch index.tsnpm run devpnpm run devyarn run devnpx tsx index.tsnode index.jsnpm run devpnpm run devyarn run dev4. Inspect the headers
Section titled “4. Inspect the headers”The default headers apply a pragmatic set of security headers to your application, but may break things (particularly the CSP header).
We recommend you test your application thoroughly and tweak the settings to ensure it continues to work as expected.
Inspect the headers with the curl command:
curl -I -X GET localhost:3000The printed headers look something like this:
HTTP/1.1 200 OKContent-Security-Policy: base-uri 'none'; child-src 'none'; connect-src 'self'; default-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'none'; frame-src 'none'; img-src 'self' blob: data:; manifest-src 'self'; media-src 'self'; object-src 'none'; script-src 'self'; style-src 'self'; worker-src 'self'; upgrade-insecure-requests;Cross-Origin-Embedder-Policy: require-corpCross-Origin-Opener-Policy: same-originCross-Origin-Resource-Policy: same-originReferrer-Policy: no-referrerX-Content-Type-Options: nosniffX-DNS-Prefetch-Control: offX-Frame-Options: SAMEORIGINX-XSS-Protection: 0origin-agent-cluster: ?1x-download-options: noopenx-permitted-cross-domain-policies: nonecontent-type: text/plain;charset=utf-8Date: Wed, 27 Nov 2024 20:13:30 GMTContent-Length: 11Inspect the headers with the curl command:
curl -I -X GET localhost:3000The printed headers look something like this:
HTTP/1.1 200 OKcontent-security-policy: base-uri 'none'; child-src 'none'; connect-src 'self'; default-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'none'; frame-src 'none'; img-src 'self' blob: data:; manifest-src 'self'; media-src 'self'; object-src 'none'; script-src 'self'; style-src 'self'; worker-src 'self'; upgrade-insecure-requests;cross-origin-embedder-policy: require-corpcross-origin-opener-policy: same-origincross-origin-resource-policy: same-originorigin-agent-cluster: ?1referrer-policy: no-referrerstrict-transport-security: max-age=31536000; includeSubDomainsx-content-type-options: nosniffx-dns-prefetch-control: offx-download-options: noopenx-frame-options: SAMEORIGINx-permitted-cross-domain-policies: nonex-xss-protection: 0content-type: text/plain;charset=UTF-8vary: Accept-Encodingcontent-length: 13date: Wed, 27 Nov 2024 20:21:41 GMTInspect the headers with the curl command:
curl -I -X GET localhost:3000The printed headers look something like this:
HTTP/1.1 200 OKcontent-security-policy: base-uri 'none'; child-src 'none'; connect-src 'self'; default-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'none'; frame-src 'none'; img-src 'self' blob: data:; manifest-src 'self'; media-src 'self'; object-src 'none'; script-src 'nonce-YWUwNGIwMTMtOTgwYi00MWNiLWI3M2QtMjBkYWQwODMxZTNi' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; worker-src 'self'; upgrade-insecure-requests;cross-origin-embedder-policy: require-corpcross-origin-opener-policy: same-origincross-origin-resource-policy: same-originorigin-agent-cluster: ?1referrer-policy: no-referrerstrict-transport-security: max-age=31536000; includeSubDomainsx-content-type-options: nosniffx-dns-prefetch-control: offx-download-options: noopenx-frame-options: SAMEORIGINx-permitted-cross-domain-policies: nonex-xss-protection: 0Vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch, Accept-Encodinglink: </_next/static/media/a34f9d1faa5f3315-s.p.woff2>; rel=preload; as="font"; crossorigin=""; nonce="YWUwNGIwMTMtOTgwYi00MWNiLWI3M2QtMjBkYWQwODMxZTNi"; type="font/woff2", </vercel.svg>; rel=preload; as="image", </next.svg>; rel=preload; as="image", </_next/static/css/app/layout.css?v=1732741292630>; rel=preload; as="style"; nonce="YWUwNGIwMTMtOTgwYi00MWNiLWI3M2QtMjBkYWQwODMxZTNi"Cache-Control: no-store, must-revalidateX-Powered-By: Next.jsContent-Type: text/html; charset=utf-8Date: Wed, 27 Nov 2024 21:01:32 GMTConnection: keep-aliveKeep-Alive: timeout=5Transfer-Encoding: chunkedInspect the headers with the curl command:
curl -I -X GET localhost:3000The printed headers look something like this:
HTTP/1.1 200 OKcontent-security-policy: base-uri 'none'; child-src 'none'; connect-src 'self'; default-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'none'; frame-src 'none'; img-src 'self' blob: data:; manifest-src 'self'; media-src 'self'; object-src 'none'; script-src 'self'; style-src 'self'; worker-src 'self'; upgrade-insecure-requests;cross-origin-embedder-policy: require-corpcross-origin-opener-policy: same-origincross-origin-resource-policy: same-originorigin-agent-cluster: ?1referrer-policy: no-referrerstrict-transport-security: max-age=31536000; includeSubDomainsx-content-type-options: nosniffx-dns-prefetch-control: offx-download-options: noopenx-frame-options: SAMEORIGINx-permitted-cross-domain-policies: nonex-xss-protection: 0Content-Type: text/plainDate: Wed, 27 Nov 2024 21:05:50 GMTConnection: keep-aliveKeep-Alive: timeout=5Transfer-Encoding: chunkedInspect the headers with the curl command:
curl -I -X GET localhost:5173The printed headers look something like this:
HTTP/1.1 200 OKAccess-Control-Allow-Origin: *content-length: 1475content-security-policy: child-src 'none'; default-src 'self'; frame-src 'none'; worker-src 'self'; connect-src 'self'; font-src 'self'; img-src 'self' blob: data:; manifest-src 'self'; media-src 'self'; object-src 'none'; script-src 'self' 'nonce-RsMu23BBsCD1PV101d7Prg=='; style-src 'self' 'unsafe-inline'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requestscontent-type: text/htmlcross-origin-embedder-policy: require-corpcross-origin-opener-policy: same-origincross-origin-resource-policy: same-originetag: "bz71zn"origin-agent-cluster: ?1referrer-policy: no-referrerstrict-transport-security: max-age=31536000; includeSubDomainsx-content-type-options: nosniffx-dns-prefetch-control: offx-download-options: noopenx-frame-options: SAMEORIGINx-permitted-cross-domain-policies: nonex-sveltekit-page: truex-xss-protection: 0Date: Wed, 27 Nov 2024 15:23:13 GMTConnection: keep-aliveKeep-Alive: timeout=5What next?
Section titled “What next?”Explore
Section titled “Explore”Arcjet can protect your entire app or individual routes with a few lines of code. Using the main Arcjet SDK you can set up bot protection, rate limiting for your API, minimize fraudulent registrations with the signup form protection and more.
Get help
Section titled “Get help”Need help with anything? Email support@arcjet.com to get support from our engineering team, join our Discord, or open an issue on GitHub.