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
2 changes: 1 addition & 1 deletion .fallowrc.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/fallow-rs/fallow/main/schema.json",
"entry": ["test/features/*.test.js"],
"entry": ["test/features/*.test.js", "test/unit/*.test.js"],
"ignoreDependencies": ["pino"],
"ignoreExports": [{ "file": "static/auth-client.js", "exports": ["*"] }]
}
18 changes: 12 additions & 6 deletions src/app/utils/redirect.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import config from "../../config.js";

/**
* Checks if a redirect URL is valid (in the allowlist)
* @param {string} redirectUrl - The URL to validate
* @returns {boolean} - True if valid, false otherwise
*/
const matchesGlob = (url, pattern) => {
if (!pattern.includes("*")) return false;
const escaped = pattern
.replace(/[.+?^${}()|[\]\\]/g, "\\$&")
.replace(/\*/g, ".*");
const regex = new RegExp(`^${escaped}$`);
return regex.test(url);
};

const isValidRedirectUrl = (redirectUrl) => {
if (!redirectUrl || typeof redirectUrl !== "string") {
return false;
Expand All @@ -15,7 +19,9 @@ const isValidRedirectUrl = (redirectUrl) => {
return false;
}

return config.allowed_redirects.includes(trimmed);
return config.allowed_redirects.some(
(pattern) => trimmed === pattern || matchesGlob(trimmed, pattern),
);
};

/**
Expand Down
2 changes: 1 addition & 1 deletion src/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export { db };

export const auth = betterAuth({
database: db,
baseURL: `http://${appConfig.host}:${appConfig.port}`,
baseURL: appConfig.base_url,
logger: {
disabled: false,
level: "debug",
Expand Down
3 changes: 1 addition & 2 deletions src/config.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
const config = {
port: process.env.PORT || 3000,
host: "localhost",
base_url: process.env.CODEBAR_AUTH_URL || "http://localhost:3000",
database_url: process.env.DATABASE_URL || "./auth.db",
// TODO(till): a wildcard pattern would be nice here
allowed_redirects: ["http://localhost:3000/demo"],
social: {
github: {
Expand Down
4 changes: 2 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ const server = serve(
fetch: app.fetch,
port: appConfig.port,
},
(info) => {
console.log(`Server is running on http://${appConfig.host}:${info.port}`);
() => {
console.log(`Server is running on ${appConfig.base_url}`);
},
);

Expand Down
104 changes: 104 additions & 0 deletions test/unit/redirect.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { test } from "tap";
import { validateRedirectUrl } from "../../src/app/utils/redirect.js";
import config from "../../src/config.js";

const original = config.allowed_redirects;

test("redirect validation - exact match", async (t) => {
config.allowed_redirects = ["http://localhost:3000/demo"];

t.after(() => {
config.allowed_redirects = original;
});

t.equal(
validateRedirectUrl("http://localhost:3000/demo"),
"http://localhost:3000/demo",
);
t.equal(validateRedirectUrl("http://evil.com/steal"), "/profile");
t.equal(validateRedirectUrl(""), "/profile");
t.equal(validateRedirectUrl(null), "/profile");
t.equal(validateRedirectUrl(" "), "/profile");
});

test("allows subdomain wildcard", async (t) => {
config.allowed_redirects = ["https://*.codebar.io"];

t.after(() => {
config.allowed_redirects = original;
});

t.equal(
validateRedirectUrl("https://auth.codebar.io"),
"https://auth.codebar.io",
);
t.equal(
validateRedirectUrl("https://staging.codebar.io"),
"https://staging.codebar.io",
);
t.equal(
validateRedirectUrl("https://app.codebar.io"),
"https://app.codebar.io",
);
});

test("rejects non-matching domain with wildcard", async (t) => {
config.allowed_redirects = ["https://*.codebar.io"];

t.after(() => {
config.allowed_redirects = original;
});

t.equal(validateRedirectUrl("https://codebar.io"), "/profile");
t.equal(validateRedirectUrl("https://evil.com"), "/profile");
});

test("allows path wildcard", async (t) => {
config.allowed_redirects = ["https://codebar.io/*"];

t.after(() => {
config.allowed_redirects = original;
});

t.equal(
validateRedirectUrl("https://codebar.io/profile"),
"https://codebar.io/profile",
);
t.equal(
validateRedirectUrl("https://codebar.io/anything/here"),
"https://codebar.io/anything/here",
);
});

test("allows multiple wildcards", async (t) => {
config.allowed_redirects = ["https://*.example.com/*"];

t.after(() => {
config.allowed_redirects = original;
});

t.equal(
validateRedirectUrl("https://auth.example.com/page"),
"https://auth.example.com/page",
);
t.equal(
validateRedirectUrl("https://api.example.com/v1/users"),
"https://api.example.com/v1/users",
);
});

test("exact match takes precedence over wildcard", async (t) => {
config.allowed_redirects = [
"http://localhost:3000/demo",
"http://localhost:3000/*",
];

t.after(() => {
config.allowed_redirects = original;
});

t.equal(
validateRedirectUrl("http://localhost:3000/demo"),
"http://localhost:3000/demo",
);
});