-
Notifications
You must be signed in to change notification settings - Fork 0
[WIP] Address feedback on 'The Tale of the Stabilized Deployment' PR #20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ | |
| !.yarn/plugins | ||
| !.yarn/releases | ||
| !.yarn/versions | ||
| package-lock.json | ||
|
|
||
| # testing | ||
| /coverage | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| import { describe, it, expect } from "vitest" | ||
| import { isSafeRedirectUrl } from "../url-validation" | ||
|
|
||
| describe("isSafeRedirectUrl", () => { | ||
| describe("should return true for safe relative URLs", () => { | ||
| it("accepts simple paths", () => { | ||
| expect(isSafeRedirectUrl("/dashboard")).toBe(true) | ||
| expect(isSafeRedirectUrl("/configure")).toBe(true) | ||
| expect(isSafeRedirectUrl("/settings")).toBe(true) | ||
| }) | ||
|
|
||
| it("accepts paths with query parameters", () => { | ||
| expect(isSafeRedirectUrl("/dashboard?tab=active")).toBe(true) | ||
| expect(isSafeRedirectUrl("/configure?step=2")).toBe(true) | ||
| }) | ||
|
|
||
| it("accepts paths with hash fragments", () => { | ||
| expect(isSafeRedirectUrl("/dashboard#section")).toBe(true) | ||
| expect(isSafeRedirectUrl("/page#top")).toBe(true) | ||
| }) | ||
|
|
||
| it("accepts nested paths", () => { | ||
| expect(isSafeRedirectUrl("/dashboard/deployments/123")).toBe(true) | ||
| expect(isSafeRedirectUrl("/a/b/c/d/e")).toBe(true) | ||
| }) | ||
| }) | ||
|
|
||
| describe("should return false for unsafe URLs", () => { | ||
| it("rejects absolute URLs with http protocol", () => { | ||
| expect(isSafeRedirectUrl("http://evil.com")).toBe(false) | ||
| expect(isSafeRedirectUrl("http://example.com/path")).toBe(false) | ||
| }) | ||
|
|
||
| it("rejects absolute URLs with https protocol", () => { | ||
| expect(isSafeRedirectUrl("https://evil.com")).toBe(false) | ||
| expect(isSafeRedirectUrl("https://example.com/path")).toBe(false) | ||
| }) | ||
|
|
||
| it("rejects protocol-relative URLs", () => { | ||
| expect(isSafeRedirectUrl("//evil.com")).toBe(false) | ||
| expect(isSafeRedirectUrl("//example.com/path")).toBe(false) | ||
| }) | ||
|
|
||
| it("rejects javascript: URLs", () => { | ||
| expect(isSafeRedirectUrl("javascript:alert(1)")).toBe(false) | ||
| expect(isSafeRedirectUrl("javascript:void(0)")).toBe(false) | ||
| }) | ||
|
|
||
| it("rejects data: URLs", () => { | ||
| expect(isSafeRedirectUrl("data:text/html,<script>alert(1)</script>")).toBe(false) | ||
| }) | ||
|
|
||
| it("rejects URLs not starting with /", () => { | ||
| expect(isSafeRedirectUrl("dashboard")).toBe(false) | ||
| expect(isSafeRedirectUrl("./dashboard")).toBe(false) | ||
| expect(isSafeRedirectUrl("../dashboard")).toBe(false) | ||
| }) | ||
|
|
||
| it("rejects other malicious protocols", () => { | ||
| expect(isSafeRedirectUrl("file:///etc/passwd")).toBe(false) | ||
| expect(isSafeRedirectUrl("ftp://server.com/file")).toBe(false) | ||
| }) | ||
| }) | ||
|
|
||
| describe("edge cases", () => { | ||
| it("handles empty strings", () => { | ||
| expect(isSafeRedirectUrl("")).toBe(false) | ||
| }) | ||
|
|
||
| it("handles URLs with encoded characters", () => { | ||
| expect(isSafeRedirectUrl("/path?url=http%3A%2F%2Fevil.com")).toBe(true) // encoded as query param is OK | ||
| expect(isSafeRedirectUrl("/path%2F..%2F..%2Fetc")).toBe(true) // path traversal encoded is still a relative path | ||
| }) | ||
|
|
||
| it("rejects URLs trying to bypass with protocols in path", () => { | ||
| // These should still be rejected as they contain :// | ||
| expect(isSafeRedirectUrl("/redirect?url=https://evil.com")).toBe(false) | ||
| }) | ||
| }) | ||
| }) | ||
|
Comment on lines
+65
to
+80
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,41 @@ | ||||||||||||||||
| /** | ||||||||||||||||
| * Validates if a URL is safe for redirecting within the application. | ||||||||||||||||
| * | ||||||||||||||||
| * A URL is considered safe if it: | ||||||||||||||||
| * 1. Is a relative path starting with "/" | ||||||||||||||||
| * 2. Does not contain protocol markers (like "://") which indicate absolute URLs | ||||||||||||||||
| * 3. Does not start with "//" (protocol-relative URLs) | ||||||||||||||||
| * | ||||||||||||||||
| * This prevents open redirect vulnerabilities where attackers could redirect | ||||||||||||||||
| * users to malicious external sites. | ||||||||||||||||
| * | ||||||||||||||||
| * @param url - The URL string to validate | ||||||||||||||||
| * @returns true if the URL is safe for internal redirects, false otherwise | ||||||||||||||||
| * | ||||||||||||||||
| * @example | ||||||||||||||||
| * ```ts | ||||||||||||||||
| * isSafeRedirectUrl("/dashboard") // true | ||||||||||||||||
| * isSafeRedirectUrl("/dashboard?tab=active") // true | ||||||||||||||||
| * isSafeRedirectUrl("https://evil.com") // false | ||||||||||||||||
| * isSafeRedirectUrl("//evil.com") // false | ||||||||||||||||
| * isSafeRedirectUrl("javascript:alert(1)") // false | ||||||||||||||||
| * ``` | ||||||||||||||||
| */ | ||||||||||||||||
| export function isSafeRedirectUrl(url: string): boolean { | ||||||||||||||||
| // Must start with "/" to be a relative path | ||||||||||||||||
| if (!url.startsWith("/")) { | ||||||||||||||||
| return false | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // Must not start with "//" (protocol-relative URL) | ||||||||||||||||
| if (url.startsWith("//")) { | ||||||||||||||||
| return false | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // Must not contain "://" which indicates an absolute URL with protocol | ||||||||||||||||
| if (url.includes("://")) { | ||||||||||||||||
|
Comment on lines
+35
to
+36
|
||||||||||||||||
| // Must not contain "://" which indicates an absolute URL with protocol | |
| if (url.includes("://")) { | |
| // Must not contain "://" in the path portion (before any "?"), which indicates an absolute URL with protocol | |
| const queryIndex = url.indexOf("?") | |
| const pathPart = queryIndex === -1 ? url : url.slice(0, queryIndex) | |
| if (pathPart.includes("://")) { |
Copilot
AI
Jan 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The validation does not check for backslash-based bypass attempts. In some browsers and URL parsing contexts, backslashes can be normalized to forward slashes. For example, URLs like /\example.com or /\\example.com might be interpreted as //example.com in certain contexts, creating a protocol-relative URL bypass.
Consider adding validation to reject URLs containing backslashes, or at minimum, add test coverage for these edge cases to verify the behavior of the new URL() constructor in Next.js middleware with backslash-containing URLs.
| } | |
| } | |
| // Must not contain backslashes, which some parsers normalize to "/" | |
| if (url.includes("\\")) { | |
| return false | |
| } |
Copilot
AI
Jan 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The validation does not handle URLs with leading or trailing whitespace, or URLs containing tab/newline characters. While the middleware extracts the callbackUrl from URL search parameters (which typically handles URL encoding), it's a security best practice to explicitly trim and sanitize the input.
Consider adding url.trim() at the beginning of the validation function, and potentially checking for or stripping control characters (tabs, newlines, etc.) that could be used in bypass attempts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test has an incorrect expectation. The URL
/redirect?url=https://evil.comis actually a safe relative path - it redirects to the/redirectpage withhttps://evil.comas a query parameter value. The current validation function correctly identifies this as unsafe due to theincludes("://")check, but this is a false positive that would break legitimate use cases.The test comment states "These should still be rejected as they contain ://", but this reasoning is flawed - the presence of "://" in query parameter values does not make a relative URL unsafe. Update this test to expect
trueinstead offalse, or remove it if the validation logic is fixed to properly handle query parameters.