diff --git a/apps/mobile/app/(tabs)/(event-types)/event-type-detail.tsx b/apps/mobile/app/(tabs)/(event-types)/event-type-detail.tsx index 193607f..ecb2057 100644 --- a/apps/mobile/app/(tabs)/(event-types)/event-type-detail.tsx +++ b/apps/mobile/app/(tabs)/(event-types)/event-type-detail.tsx @@ -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"; @@ -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"; @@ -1278,6 +1285,7 @@ export default function EventTypeDetail() { currentFormState, eventTypeData, updateEventType, + successRedirectUrl, ]); const headerTitle = id === "new" ? "Create Event Type" : truncateTitle(title); diff --git a/apps/mobile/utils/redirectUrlValidation.test.js b/apps/mobile/utils/redirectUrlValidation.test.js new file mode 100644 index 0000000..0c6b21a --- /dev/null +++ b/apps/mobile/utils/redirectUrlValidation.test.js @@ -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,").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); + }); +}); diff --git a/apps/mobile/utils/redirectUrlValidation.ts b/apps/mobile/utils/redirectUrlValidation.ts new file mode 100644 index 0000000..a4e1675 --- /dev/null +++ b/apps/mobile/utils/redirectUrlValidation.ts @@ -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:") { + return { valid: true }; + } + + return { valid: false, error: "Redirect URL must use HTTP or HTTPS." }; +}