8000 fix(auth): refuse sign-in destinations with control characters by adeelahmad · Pull Request #197 · adeelahmad/lense · GitHub
[go: up one dir, main page]

Skip to content
Merged
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
4 changes: 3 additions & 1 deletion fastapi_backend/app/domain/external_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,8 +269,10 @@ def start(db, cfg, key, origin, mode="login", account=None, next_path="/"):


def _safe_next(path):
"""A path on this site. Browsers drop tabs and line breaks from a URL ("/\\t/evil.example" is "//evil.example")."""
path = path or "/"
return path if path.startswith("/") and not path.startswith("//") and "\\" not in path else "/"
ok = path.startswith("/") and not path.startswith("//") and "\\" not in path and not re.search(r"[\x00-\x1f\x7f]", path)
return path if ok else "/"


def claim(db, state, browser):
Expand Down
2 changes: 2 additions & 0 deletions fastapi_backend/tests/api/test_external_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ def test_the_round_trip_is_tied_to_this_browser_and_used_once(app, client, new_c
# where to go next is always a page of this site
got = _sign_in(client, idp, "acme-sso", ADA, next_path="//evil.example/x")
assert got["next"] == "/"
got = _sign_in(client, idp, "acme-sso", ADA, next_path="/\t/evil.example/x") # browsers drop the tab: //evil.example
assert got["next"] == "/"


def test_a_wrong_client_secret_says_so(app, client, admin, idp, db):
Expand Down
1 change: 1 addition & 0 deletions nextjs-frontend/__tests__/externalSignIn.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ describe("coming back from the provider", () => {
expect(safeNext("//evil.example")).toBe("/");
expect(safeNext("https://evil.example")).toBe("/");
expect(safeNext("/\\evil.example")).toBe("/");
expect(safeNext("/\t/evil.example")).toBe("/");
expect(safeNext("/recordings?x=1")).toBe("/recordings?x=1");
});
});
3 changes: 3 additions & 0 deletions nextjs-frontend/__tests__/navigation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ describe("safeCallbackUrl", () => {
["//evil.example", "/"],
["/\\evil.example", "/"],
["https://evil.example", "/"],
["/\t/evil.example", "/"],
["/\n/evil.example", "/"],
["/\r/evil.example", "/"],
[null, "/"],
])("%p -> %p", (input, expected) => {
expect(safeCallbackUrl(input)).toBe(expected);
Expand Down
3 changes: 2 additions & 1 deletion nextjs-frontend/components/auth/external-signin-return.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import { useEffect, useState } from "react";

import { AuthAlert, AuthCard } from "@/components/auth/auth-card";
import { finishSignIn } from "@/components/auth/passkey-flows";
import { safeCallbackUrl } from "@/lib/definitions";

/** A page of this site to go to next (never another site). */
export function safeNext(next: string | null | undefined, fallback = "/"): string {
return next && next.startsWith("/") && !next.startsWith("//") && !next.includes("\\") ? next : fallback;
return next && safeCallbackUrl(next) === next ? next : fallback;
}

function sentence(s: string): string {
Expand Down
6 changes: 4 additions & 2 deletions nextjs-frontend/lib/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ export type FormState =
}
| undefined;

/** Only same-origin paths are allowed as post-login destinations. */
/** Only same-origin paths are allowed as post-login destinations. Browsers drop tabs and line breaks from a URL, so
* "/\t/evil.example" would become "//evil.example": control characters and backslashes are refused anywhere. */
export function safeCallbackUrl(value: unknown): string {
return typeof value === "string" && /^\/(?![/\\])/.test(value) ? value : "/";
// eslint-disable-next-line no-control-regex
return typeof value === "string" && /^\/(?![/\\])/.test(value) && !/[\x00-\x1f\x7f\\]/.test(value) ? value : "/";
}
Loading
0