From 95ca730aa0efe849b22be74895c453aeb656ee8d Mon Sep 17 00:00:00 2001 From: devcluna Date: Thu, 9 Jul 2026 22:33:58 -0500 Subject: [PATCH] test(embed): cover the redirect-safety guard Extract the post-submit redirect check into a pure `isSafeRedirect(url, base)` and inject it into the served embed via `.toString()`, so the browser runs the exact function unit-tested here (no drift). Tests cover http(s)/relative allow and javascript:/data:/mailto:/ftp:/tel: block. --- server/src/controllers/form.ts | 8 +++----- server/src/lib/embed.ts | 17 +++++++++++++++++ tests/redirect.test.ts | 30 ++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 server/src/lib/embed.ts create mode 100644 tests/redirect.test.ts diff --git a/server/src/controllers/form.ts b/server/src/controllers/form.ts index 7c70828..8406570 100644 --- a/server/src/controllers/form.ts +++ b/server/src/controllers/form.ts @@ -1,4 +1,5 @@ import { FORM_CSS } from '../../../admin/src/form-css'; +import { isSafeRedirect } from '../lib/embed'; const PLUGIN_ID = 'strapi-plugin-form-builder-cms'; @@ -74,6 +75,7 @@ function publicPageHtml(form: any): string { function embedScript(): string { return `(function () { var PLUGIN = 'strapi-plugin-form-builder-cms'; + var isSafeRedirect = ${isSafeRedirect.toString()}; function init() { document.querySelectorAll('script[data-form-id]').forEach(function (script) { @@ -228,11 +230,7 @@ function embedScript(): string { // redirect after submit, if configured — only to an http(s) or relative URL, // so a "javascript:"/"data:" value can never run as an open-redirect/XSS. var redirect = form.settings && form.settings.redirectUrl; - if (redirect) { - var safe = false; - try { var ru = new URL(redirect, window.location.href); safe = ru.protocol === 'http:' || ru.protocol === 'https:'; } catch (e) { safe = false; } - if (safe) { window.location.href = redirect; return; } - } + if (redirect && isSafeRedirect(redirect, window.location.href)) { window.location.href = redirect; return; } // otherwise show the success message — wrapped in .sfb-form + re-applying the // theme so it stays styled (the themed
we just cleared is gone) var wrap = document.createElement('div'); diff --git a/server/src/lib/embed.ts b/server/src/lib/embed.ts new file mode 100644 index 0000000..9869e3f --- /dev/null +++ b/server/src/lib/embed.ts @@ -0,0 +1,17 @@ +/** + * Pure helpers for the embed runtime. These are injected into the served embed + * script via `.toString()`, so the browser runs the exact code covered by tests + * here — no drift between the tested function and the shipped one. Keep them + * self-contained (globals only, no imports) so the stringified source runs + * standalone in the browser. + */ + +/** Is a post-submit redirect target safe to navigate to (http/https/relative only)? */ +export function isSafeRedirect(url: string, base: string): boolean { + try { + const u = new URL(url, base); + return u.protocol === 'http:' || u.protocol === 'https:'; + } catch (e) { + return false; + } +} diff --git a/tests/redirect.test.ts b/tests/redirect.test.ts new file mode 100644 index 0000000..8e35fdb --- /dev/null +++ b/tests/redirect.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from 'vitest'; +import { isSafeRedirect } from '../server/src/lib/embed'; + +const BASE = 'https://acme.co/contact'; + +describe('isSafeRedirect (post-submit redirect guard)', () => { + it('allows absolute http(s) URLs', () => { + expect(isSafeRedirect('https://acme.co/thanks', BASE)).toBe(true); + expect(isSafeRedirect('http://acme.co/thanks', BASE)).toBe(true); + }); + + it('allows relative paths (resolved against the current page)', () => { + expect(isSafeRedirect('/thanks', BASE)).toBe(true); + expect(isSafeRedirect('thank-you', BASE)).toBe(true); + }); + + it('blocks javascript: and data: URLs (XSS / open-redirect)', () => { + expect(isSafeRedirect('javascript:alert(1)', BASE)).toBe(false); + expect(isSafeRedirect('JavaScript:alert(1)', BASE)).toBe(false); + expect(isSafeRedirect('data:text/html,', BASE)).toBe(false); + }); + + it('blocks other non-http protocols', () => { + expect(isSafeRedirect('mailto:x@y.com', BASE)).toBe(false); + expect(isSafeRedirect('ftp://acme.co/f', BASE)).toBe(false); + expect(isSafeRedirect('tel:+123', BASE)).toBe(false); + }); + // note: an empty string is filtered upstream by the `redirect && …` guard in the embed, + // so it never reaches isSafeRedirect (where, against a base, it would resolve to the page). +});