Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,15 @@ These are intentional exclusions:
- **Node.js production server (`vinext start`)** works for testing but is less complete than Workers deployment. Cloudflare Workers is the primary target.
- **Native Node modules (sharp, resvg, satori, lightningcss, @napi-rs/canvas)** crash Vite's RSC dev environment. Dynamic OG image/icon routes using these work in production builds but not in dev mode. These are auto-stubbed during `vinext deploy`.

## Security hardening

vinext handles URL normalization, path traversal prevention, and internal header stripping automatically. For production deployments, we recommend adding a `proxy.ts` (Next.js 16) or `middleware.ts` (Next.js 15) with:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the phrase "we recommend" is a strong endorsement that implies this is necessary for security. Since the README sentence right before it says vinext already handles URL normalization, path traversal prevention, and internal header stripping automatically, users might wonder why they need to add a proxy.ts at all.

Consider softening to something like "you may want to add" and clarifying that the built-in protections handle the critical stuff, while these headers are best-practice additions:

Suggested change
vinext handles URL normalization, path traversal prevention, and internal header stripping automatically. For production deployments, we recommend adding a `proxy.ts` (Next.js 16) or `middleware.ts` (Next.js 15) with:
vinext handles URL normalization, path traversal prevention, and internal header stripping automatically. For production deployments, you may also want to add a `proxy.ts` (Next.js 16) or `middleware.ts` (Next.js 15) with:


1. **Security response headers** — `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, `Permissions-Policy`
2. **Explicit route matcher** — ensures the proxy runs on all paths including `/api/*`

See [`examples/app-router-cloudflare/proxy.ts`](examples/app-router-cloudflare/proxy.ts) for a ready-to-use template.

## Benchmarks

> **Caveat:** Benchmarks are hard to get right and these are early results. Take them as directional, not definitive.
Expand Down
47 changes: 47 additions & 0 deletions examples/app-router-cloudflare/proxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { NextRequest, NextResponse } from 'next/server';

/**
* Security-hardened proxy example for vinext on Cloudflare Workers.
*
* This proxy adds:
* 1. Security response headers (OWASP recommended)
* 2. Double-encoded path traversal protection
*
* See: https://owasp.org/www-project-secure-headers/
*/
export default function proxy(request: NextRequest) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: vinext supports both export default and a named proxy export for proxy files (matching Next.js 16 convention). Since this is meant as a best-practice template, consider using the named export to be explicit about the Next.js 16 convention:

Suggested change
export default function proxy(request: NextRequest) {
export default function proxy(request: NextRequest) {

Actually, export default is fine and works correctly — just noting that export function proxy(...) is also valid if you want to align with the named-export convention mentioned in the vinext middleware runner.

// Block double-encoded path traversal attempts.
// %252f = double-encoded '/', %2e%2e = encoded '..', %5c = encoded '\'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says %2e%2e = encoded '..' but the regex on line 17 doesn't match %2e%2e — it matches %25xx (double-encoded). These are different things. Single-encoded %2e%2e (..) is already handled by vinext's built-in normalizePath. The comment should be updated to accurately reflect what the regex catches, or removed if the check is removed.

Suggested change
// %252f = double-encoded '/', %2e%2e = encoded '..', %5c = encoded '\'
// Block double-encoded path traversal attempts (%25xx) and encoded backslashes (%5c).
// Single-encoded sequences like %2e%2e are already handled by vinext's built-in normalization.

// These can bypass route matching when the server decodes at different stages.
const rawUrl = request.url;
if (/%25[0-9a-fA-F]{2}/.test(rawUrl) || /%5[cC]/.test(rawUrl)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This regex tests against the full serialized request.url (including origin, query string, and fragment), not the raw HTTP request line. It will produce false positives for legitimate URLs — e.g., a query string containing %25 (a literal percent sign) or %5c (a backslash in user input).

Also, by the time this code runs, the URL has already been parsed and re-serialized through a URL constructor in vinext's runMiddleware, so the encoding may not match what was originally sent by the client.

Suggestion: either remove this check (vinext handles path traversal built-in via decodeURIComponent + normalizePath in the middleware runner) or scope it to just the pathname:

Suggested change
if (/%25[0-9a-fA-F]{2}/.test(rawUrl) || /%5[cC]/.test(rawUrl)) {
const { pathname } = new URL(request.url);
if (/%25[0-9a-fA-F]{2}/.test(pathname) || /%5[cC]/.test(pathname)) {

return new NextResponse('Bad Request', { status: 400 });
}

const response = NextResponse.next();

// Prevent MIME-type sniffing
response.headers.set('X-Content-Type-Options', 'nosniff');

// Prevent clickjacking — use 'SAMEORIGIN' if you embed your own pages in iframes
response.headers.set('X-Frame-Options', 'DENY');

// Control referrer information sent to other origins
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');

// Restrict browser features — customize based on your app's needs
response.headers.set(
'Permissions-Policy',
'camera=(), microphone=(), geolocation=()',
);

return response;
}

export const config = {
matcher: [
// Match all paths except static assets and vinext internals.
// This explicit matcher ensures /api/* routes are also covered.
'/((?!_vinext|_next/static|favicon\\.ico).*)',
],
};
Loading