-
Notifications
You must be signed in to change notification settings - Fork 27.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
allow passing wildcard domains in serverActions.allowedDomains (#59428)
Implementation of feature request opened here - #59427 Approach: ~~We are using micromatch in the csrf protection step of actionHandler to allow for wildcard domains passed in allowedDomains. This is the same library used for matching domains for remote images.~~ If any of the allowed domains match the origin of the request, we skip the downstream error thrown for csrf protection. Edit: Micromatch is not available in this context as it is only compatible with Node. This codepath can be run from the edge, so we need to rely on vanilla js compatible code only. Instead of falling back to allowing the user to pass in a regex, which can be somewhat insecure, we opt into continuing to use a wildcard pattern from a configuration standpoint and instead use a simple function that matches on wildcards using string comparison and iteration. Ideally, Micromatch can be retrofitted to work in non-Node settings and this piece of code can be replaced in the future, without deprecating or changing the next.config interface. --------- Co-authored-by: Josh Story <story@hey.com>
- Loading branch information
Showing
6 changed files
with
135 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
42 changes: 42 additions & 0 deletions
42
packages/next/src/server/app-render/csrf-protection.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import { isCsrfOriginAllowed } from './csrf-protection' | ||
|
||
describe('isCsrfOriginAllowed', () => { | ||
it('should return true when allowedOrigins contains originDomain', () => { | ||
expect(isCsrfOriginAllowed('vercel.com', ['vercel.com'])).toBe(true) | ||
expect(isCsrfOriginAllowed('www.vercel.com', ['www.vercel.com'])).toBe(true) | ||
}) | ||
|
||
it('should return true when allowedOrigins contains originDomain with matching pattern', () => { | ||
expect(isCsrfOriginAllowed('asdf.vercel.com', ['*.vercel.com'])).toBe(true) | ||
expect(isCsrfOriginAllowed('asdf.vercel.com', ['**.vercel.com'])).toBe(true) | ||
expect(isCsrfOriginAllowed('asdf.jkl.vercel.com', ['**.vercel.com'])).toBe( | ||
true | ||
) | ||
}) | ||
|
||
it('should return false when allowedOrigins contains originDomain with non-matching pattern', () => { | ||
expect(isCsrfOriginAllowed('asdf.vercel.com', ['*.vercel.app'])).toBe(false) | ||
expect(isCsrfOriginAllowed('asdf.jkl.vercel.com', ['*.vercel.com'])).toBe( | ||
false | ||
) | ||
expect(isCsrfOriginAllowed('asdf.jkl.vercel.app', ['**.vercel.com'])).toBe( | ||
false | ||
) | ||
}) | ||
|
||
it('should return false when allowedOrigins does not contain originDomain', () => { | ||
expect(isCsrfOriginAllowed('vercel.com', ['nextjs.org'])).toBe(false) | ||
}) | ||
|
||
it('should return false when allowedOrigins is undefined', () => { | ||
expect(isCsrfOriginAllowed('vercel.com', undefined)).toBe(false) | ||
}) | ||
|
||
it('should return false when allowedOrigins is empty', () => { | ||
expect(isCsrfOriginAllowed('vercel.com', [])).toBe(false) | ||
}) | ||
|
||
it('should return false when allowedOrigins is empty string', () => { | ||
expect(isCsrfOriginAllowed('vercel.com', [''])).toBe(false) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
// micromatch is only available at node runtime, so it cannot be used here since the code path that calls this function | ||
// can be run from edge. This is a simple implementation that safely achieves the required functionality. | ||
// the goal is to match the functionality for remotePatterns as defined here - | ||
// https://nextjs.org/docs/app/api-reference/components/image#remotepatterns | ||
// TODO - retrofit micromatch to work in edge and use that instead | ||
function matchWildcardDomain(domain: string, pattern: string) { | ||
const domainParts = domain.split('.') | ||
const patternParts = pattern.split('.') | ||
|
||
if (patternParts.length < 1) { | ||
// pattern is empty and therefore invalid to match against | ||
return false | ||
} | ||
|
||
if (domainParts.length < patternParts.length) { | ||
// domain has too few segments and thus cannot match | ||
return false | ||
} | ||
|
||
let depth = 0 | ||
while (patternParts.length && depth++ < 2) { | ||
const patternPart = patternParts.pop() | ||
const domainPart = domainParts.pop() | ||
|
||
switch (patternPart) { | ||
case '': | ||
case '*': | ||
case '**': { | ||
// invalid pattern. pattern segments must be non empty | ||
// Additionally wildcards are only supported below the domain level | ||
return false | ||
} | ||
default: { | ||
if (domainPart !== patternPart) { | ||
return false | ||
} | ||
} | ||
} | ||
} | ||
|
||
while (patternParts.length) { | ||
const patternPart = patternParts.pop() | ||
const domainPart = domainParts.pop() | ||
|
||
switch (patternPart) { | ||
case '': { | ||
// invalid pattern. pattern segments must be non empty | ||
return false | ||
} | ||
case '*': { | ||
// wildcard matches anything so we continue if the domain part is non-empty | ||
if (domainPart) { | ||
continue | ||
} else { | ||
return false | ||
} | ||
} | ||
case '**': { | ||
// if this is not the last item in the pattern the pattern is invalid | ||
if (patternParts.length > 0) { | ||
return false | ||
} | ||
// recursive wildcard matches anything so we terminate here if the domain part is non empty | ||
return domainPart !== undefined | ||
} | ||
default: { | ||
if (domainPart !== patternPart) { | ||
return false | ||
} | ||
} | ||
} | ||
} | ||
|
||
// We exhausted the pattern. If we also exhausted the domain we have a match | ||
return domainParts.length === 0 | ||
} | ||
|
||
export const isCsrfOriginAllowed = ( | ||
originDomain: string, | ||
allowedOrigins: string[] = [] | ||
): boolean => { | ||
return allowedOrigins.some( | ||
(allowedOrigin) => | ||
allowedOrigin && | ||
(allowedOrigin === originDomain || | ||
matchWildcardDomain(originDomain, allowedOrigin)) | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters