Merge branch 'main' into docs/utils-jsdoc

This commit is contained in:
Ola Hungerford 2025-08-27 20:13:01 -07:00 • committed by GitHub
commit 73873e2b77
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 1520 additions and 665 deletions

View file

@ -0,0 +1,135 @@
import { validateRedirectUrl } from "../urlValidation";
describe("validateRedirectUrl", () => {
describe("valid URLs", () => {
it("should allow HTTP URLs", () => {
expect(() => validateRedirectUrl("http://example.com")).not.toThrow();
});
it("should allow HTTPS URLs", () => {
expect(() => validateRedirectUrl("https://example.com")).not.toThrow();
});
it("should allow URLs with ports", () => {
expect(() =>
validateRedirectUrl("https://example.com:8080"),
).not.toThrow();
});
it("should allow URLs with paths", () => {
expect(() =>
validateRedirectUrl("https://example.com/path/to/auth"),
).not.toThrow();
});
it("should allow URLs with query parameters", () => {
expect(() =>
validateRedirectUrl("https://example.com?param=value"),
).not.toThrow();
});
});
describe("invalid URLs - XSS vectors", () => {
it("should block javascript: protocol", () => {
expect(() => validateRedirectUrl("javascript:alert('XSS')")).toThrow(
"Authorization URL must be HTTP or HTTPS",
);
});
it("should block javascript: with encoded characters", () => {
expect(() =>
validateRedirectUrl("javascript:alert%28%27XSS%27%29"),
).toThrow("Authorization URL must be HTTP or HTTPS");
});
it("should block data: protocol", () => {
expect(() =>
validateRedirectUrl("data:text/html,<script>alert('XSS')</script>"),
).toThrow("Authorization URL must be HTTP or HTTPS");
});
it("should block vbscript: protocol", () => {
expect(() => validateRedirectUrl("vbscript:msgbox")).toThrow(
"Authorization URL must be HTTP or HTTPS",
);
});
it("should block file: protocol", () => {
expect(() => validateRedirectUrl("file:///etc/passwd")).toThrow(
"Authorization URL must be HTTP or HTTPS",
);
});
it("should block about: protocol", () => {
expect(() => validateRedirectUrl("about:blank")).toThrow(
"Authorization URL must be HTTP or HTTPS",
);
});
it("should block custom protocols", () => {
expect(() => validateRedirectUrl("custom://example")).toThrow(
"Authorization URL must be HTTP or HTTPS",
);
});
});
describe("edge cases", () => {
it("should handle malformed URLs", () => {
expect(() => validateRedirectUrl("not a url")).toThrow(
"Invalid URL: not a url",
);
});
it("should handle empty string", () => {
expect(() => validateRedirectUrl("")).toThrow("Invalid URL: ");
});
it("should handle URLs with unicode characters", () => {
expect(() => validateRedirectUrl("https://例え.jp")).not.toThrow();
});
it("should handle URLs with case variations", () => {
expect(() => validateRedirectUrl("HTTPS://EXAMPLE.COM")).not.toThrow();
expect(() => validateRedirectUrl("HtTpS://example.com")).not.toThrow();
});
it("should handle protocol-relative URLs as invalid", () => {
expect(() => validateRedirectUrl("//example.com")).toThrow(
"Invalid URL: //example.com",
);
});
it("should handle URLs with authentication", () => {
expect(() =>
validateRedirectUrl("https://user:pass@example.com"),
).not.toThrow();
});
});
describe("security considerations", () => {
it("should not be fooled by whitespace", () => {
expect(() => validateRedirectUrl(" javascript:alert('XSS')")).toThrow();
expect(() => validateRedirectUrl("javascript:alert('XSS') ")).toThrow();
});
it("should handle null bytes", () => {
expect(() =>
validateRedirectUrl("java\x00script:alert('XSS')"),
).toThrow();
});
it("should handle tab characters", () => {
expect(() => validateRedirectUrl("java\tscript:alert('XSS')")).toThrow();
});
it("should handle newlines", () => {
expect(() => validateRedirectUrl("java\nscript:alert('XSS')")).toThrow();
});
it("should handle mixed case protocols", () => {
expect(() => validateRedirectUrl("JaVaScRiPt:alert('XSS')")).toThrow(
"Authorization URL must be HTTP or HTTPS",
);
});
});
});

View file

@ -0,0 +1,24 @@
/**
* Validates that a URL is safe for redirection.
* Only allows HTTP and HTTPS protocols to prevent XSS attacks.
*
* @param url - The URL string to validate
* @throws Error if the URL has an unsafe protocol
*/
export function validateRedirectUrl(url: string): void {
try {
const parsedUrl = new URL(url);
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
throw new Error("Authorization URL must be HTTP or HTTPS");
}
} catch (error) {
if (
error instanceof Error &&
error.message === "Authorization URL must be HTTP or HTTPS"
) {
throw error;
}
// If URL parsing fails, it's also invalid
throw new Error(`Invalid URL: ${url}`);
}
}