Skip to content
Open
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
8 changes: 8 additions & 0 deletions apps/mobile/app/(tabs)/(event-types)/event-type-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import {
mapItemToApiLocation,
validateLocationItem,
} from "@/utils/locationHelpers";
import { validateExternalRedirectUrl } from "@/utils/redirectUrlValidation";
import { getCalAppUrl } from "@/utils/region";
import { safeLogError } from "@/utils/safeLogger";

Expand Down Expand Up @@ -1204,6 +1205,12 @@ export default function EventTypeDetail() {
}
}

const redirectValidation = validateExternalRedirectUrl(successRedirectUrl);
if (!redirectValidation.valid) {
showErrorAlert("Error", redirectValidation.error || "Invalid redirect URL");
return;
}

// Detect create vs update mode
const isCreateMode = id === "new";

Expand Down Expand Up @@ -1278,6 +1285,7 @@ export default function EventTypeDetail() {
currentFormState,
eventTypeData,
updateEventType,
successRedirectUrl,
]);

const headerTitle = id === "new" ? "Create Event Type" : truncateTitle(title);
Expand Down
42 changes: 42 additions & 0 deletions apps/mobile/utils/redirectUrlValidation.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { describe, expect, test } from "@jest/globals";
import { validateExternalRedirectUrl } from "./redirectUrlValidation";

describe("validateExternalRedirectUrl", () => {
test("allows an empty redirect URL", () => {
expect(validateExternalRedirectUrl("")).toEqual({ valid: true });
expect(validateExternalRedirectUrl(" ")).toEqual({ valid: true });
});

test("allows https redirect URLs", () => {
expect(validateExternalRedirectUrl("https://example.com/thanks?booking=123")).toEqual({
valid: true,
});
});

test("allows http redirect URLs to match the web app schema", () => {
expect(validateExternalRedirectUrl("http://localhost:3000/thanks")).toEqual({ valid: true });
expect(validateExternalRedirectUrl("http://127.0.0.1:3000/thanks")).toEqual({ valid: true });
expect(validateExternalRedirectUrl("http://example.com/thanks")).toEqual({ valid: true });
});

test("rejects javascript and data URLs", () => {
expect(validateExternalRedirectUrl("javascript:alert(1)").valid).toBe(false);
expect(validateExternalRedirectUrl("data:text/html,<script>alert(1)</script>").valid).toBe(
false
);
});

test("rejects non-http redirect URLs", () => {
const result = validateExternalRedirectUrl("ftp://example.com/file");

expect(result.valid).toBe(false);
expect(result.error).toContain("http://");
});

test("rejects http URLs without an explicit double-slash prefix", () => {
expect(validateExternalRedirectUrl("https:example.com").valid).toBe(false);
expect(validateExternalRedirectUrl("http:example.com").valid).toBe(false);
expect(validateExternalRedirectUrl("https:/example.com").valid).toBe(false);
expect(validateExternalRedirectUrl("http:/example.com").valid).toBe(false);
});
});
26 changes: 26 additions & 0 deletions apps/mobile/utils/redirectUrlValidation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export interface RedirectUrlValidationResult {
valid: boolean;
error?: string;
}

export function validateExternalRedirectUrl(value: string): RedirectUrlValidationResult {
const trimmed = value.trim();
if (!trimmed) return { valid: true };

if (!/^https?:\/\//.test(trimmed)) {
return { valid: false, error: "Redirect URL must start with http:// or https://." };
}

let parsed: URL;
try {
parsed = new URL(trimmed);
} catch {
return { valid: false, error: "Redirect URL must be a valid URL." };
}

if (parsed.protocol === "http:" || parsed.protocol === "https:") {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
return { valid: true };
}

return { valid: false, error: "Redirect URL must use HTTP or HTTPS." };
}
Loading