Hey ๐,
I've been working on a new package called next-armored that helps you secure your Next.js application. My first release is out, and I'm excited to share it with you. The first tool I added is a middleware to help you configure CORS for your Next.js server.
If you're only interested in how to use it, and how to set it up, you can directly go check the ๐ tutorial section ๐.
Otherwise, let's start with a quick reminder of what Cross-Origin Resource Sharing (CORS) is and when you need to set it up in your Next.js app.
๐ I - What is the CORS policy ?
Cross-Origin Resource Sharing a web browser security feature
CORS is a security feature that allows you to control which origins are allowed to access your resources. It is enforced by web browsers through Access-control-allow-*
headers.
CORS protects users of your app from malicious sites trying to perform operations (like retrieving personal information or account deletion) on your server without their knowledge. While the request will still reach your server, if it is correctly configured, it will attach CORS security headers based on the requestโs origin.
If the request comes from your allowed origins, the browser will allow access to the serverโs response.
If the request originates from a disallowed or malicious site, the browser will block access to the response.
For example, in the simplest case of a GET request, an attacker could try to retrieve information. For other methods, such as DELETE, the browser sends a preflight request first. Based on the server's CORS configuration, the browser decides whether to proceed with the request.
For more in-depth information, check this article by a friend of mine or the MDN Documentation.
For insights into CORS vulnerabilities, explore Outpost24โs blog.
โ What is an origin, and how do you differentiate them?
An origin is defined as a combination of the protocol, host, and port of a resource. For example, the origin of https://example.com/index.html
is https://example.com
. A change in any of these three components results in a different origin.
โก๏ธ Subdomains change the host, so
https://example.com
andhttps://api.example.com
are considered different origins.
๐ ๏ธ II - When should you configure CORS for your Next.js application?
We saw that https://example.com
and https://api.example.com
are different origins. So if your server is at https://api.example.com
but your frontend app at https://example.com
, you need to configure CORS on your server to allow requests from your frontend.
Why is that? Historically, browsers enforce the Same Origin Policy (SOP), allowing requests only between the same origin for security. This policyโs limitations necessitated the introduction of CORS to enable controlled cross-origin requests, such as inter-operability between different subdomains or public APIs.
Now, what about Next.js ? If you use Next.js 13 or later with the app router (e.g., a folder like ./src/app/api), your frontend (e.g., https://your-domain.com) and API (e.g., https://your-domain.com/api) share the same origin. Here, the SOP suffices, and enabling CORS without a valid reason could introduce vulnerabilities.
So, when should you enable CORS?
-
If your API is accessed by another frontend (e.g.,
https://admin.your-domain.com
). - If your API is public, allowing access from any origin (ensure the API doesnโt handle sensitive data).
๐จ๐ผโ๐ป III - How to configure CORS for Next.js with next-armored
๐ฆ Install the next-armored package
Select your favorite package manager (e.g. pnpm) and install the next-armored
package by running:
pnpm add next-armored
๐ฅ Use the CORS middleware as your first middleware
Create or update your ./src/middleware.ts
:
import { NextRequest } from "next/server";
import { createCorsMiddleware } from "next-armored/cors";
const corsMiddleware = createCorsMiddleware({
origins: ["https://example.com", "http://localhost:5173"],
});
export function middleware(request: NextRequest) {
return corsMiddleware(request);
}
export const config = {
matcher: ["/api/:path*"],
};
In Next.js, the middleware is executed before every request matching the matcher
pattern inside the config object.
For this example, I will suppose that you just created the middleware file. So you just need to match all your api routes.
If you are using the app router, you can match all your api routes by using the '/api/:path*'
pattern.
Then import the createCorsMiddleware
function from next-armored/cors
and pass your configuration object to it. This will allow you to build the cors middleware with the origins you want to allow and pass it to the middleware function.
๐ผ Compose your CORS middleware with other middleware
If you already have middleware, compose them as follows:
import { NextRequest, NextResponse } from "next/server";
import { createCorsMiddleware } from "next-armored/cors";
const corsMiddleware = createCorsMiddleware({
origins: ["https://example.com", "http://localhost:5173"],
});
const otherMiddleware = (request: NextRequest) => {
console.log("otherMiddleware");
return NextResponse.next();
};
export function middleware(request: NextRequest) {
const response = otherMiddleware(request);
const isApi = request.nextUrl.pathname.startsWith("/api");
if (isApi) {
return corsMiddleware(request, response);
}
return response;
}
export const config = {
matcher: ["/api/:path*", "/home/:path*"],
};
If your matcher didn't previously include the api routes, you will have to add it to the matcher.
Then you can call the other middleware first and get the response.
Since the cors middleware should be applied only to the api routes, you have to check if the request is an api request and then apply the cors middleware only in that case. This can be done easily by checking the pathname of the request with request.nextUrl.pathname.startsWith('/api')
.
Finally, pass the response to the cors middleware as the 2nd argument. In this case, the cors middleware will attach the headers to the existing response instead of returning a new response.
Alternatively, use a utility to chain middleware. Here is a snippet of how you can do it but it shall be adapted to the need of each.
export type CustomMiddleware = (
request: NextRequest,
event: NextFetchEvent,
response: NextResponse
) => NextMiddlewareResult;
type MiddlewareFactory = (next: CustomMiddleware) => CustomMiddleware;
export const chainMiddlewares = (
functions: MiddlewareFactory[],
index = 0
): CustomMiddleware => {
const current = functions[index];
if (current) {
const next = chainMiddlewares(functions, index + 1);
return current(next);
}
return (
_request: NextRequest,
_event: NextFetchEvent,
response: NextResponse
) => response;
};
export const middleware = chainMiddleware([
middleware1,
middleware2,
corsMiddleware,
]);
โ๏ธ Full CORS middleware configuration
const corsMiddleware = createCorsMiddleware({
origins: ["https://example.com", "http://localhost:5173"],
methods: ["GET", "POST"],
headers: ["Content-Type"],
maxAge: 60 * 60 * 24,
optionsSuccessStatus: 204,
allowCredentials: true,
exposedHeaders: ["Content-Type"],
preflightContinue: false,
});
You have to at least specify the origins
option because keeping *
by default is not recommended as it can be a security risk. If you are building a public api, you can allow all origins by passing origins: ['*']
but you should be aware of the security implications.
You can define other options as well:
-
origins
- array of origins that are allowed to access the resource. The only one to be required. -
methods
- array of methods that are allowed to access the resource. Default value to["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]
. -
headers
- array of headers that are allowed to access the resource. Default value to["Content-Type", "Authorization"]
. -
maxAge
- number of seconds that the results of a preflight request can be cached. Default value to60 * 60 * 24
(1 day). -
optionsSuccessStatus
- status code to send for successful OPTIONS requests. Default value to204
. -
allowCredentials
- boolean value to indicate if credentials are allowed to be sent with the request. Default value totrue
. -
exposedHeaders
- array of headers that are exposed to the client. Default value to[]
. -
preflightContinue
- boolean value to indicate if it should pass the CORS preflight response to the next handler. Default value tofalse
.
๐ซ Enable/disable CORS for specific paths
Enable CORS for specific paths:
const corsMiddleware = createCorsMiddleware(
{ origins: ["https://example.com", "http://localhost:5173"] },
{
includes: [{ startsWith: "/api/v2", additionalIncludes: ["example"] }],
}
);
This will enable CORS for all paths that start with
/api/v2
and at the same time includeexample
in the pathname.
api/v2/product/example
will have CORS enabled, butapi/v1/product/example/test
will not.
Disable CORS for specific paths:
const corsMiddleware = createCorsMiddleware(
{ origins: ["https://example.com", "http://localhost:5173"] },
{
excludes: [{ startsWith: "/api/restricted" }],
}
);
In this case, all the routes starting with
/api/restricted
will not enforce CORS but the SOP (Same-Origin Policy) as the default behavior suggests.
๐ฎ IV - Why use next-armored
?
๐ Easy to use: Create the middleware with few lines.
๐ง Flexible: Customize middleware configurations to fit your needs.
๐ก๏ธ Secure: Default configurations are based on best practices and security standards.
โ Type-safe: Middleware is fully typed and compatible with Next.js and TypeScript. Also it's tree-shakable.
๐ Open source: Audit, report issues, and contribute.
๐ V - Next steps for next-armored
I started with a cors middleware for Next.js but my goal is to create a bunch of utils to help you secure your Next.js application. So I definitely plan to add more utils and more middleware to this package.
The next one will probably be a middleware to handle Content Security Policy (CSP) headers. But if you have any suggestion, or specific needs, please let me know. ๐
Top comments (0)