Skip to content

Commit 95ca730

Browse files
committed
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.
1 parent 5965767 commit 95ca730

3 files changed

Lines changed: 50 additions & 5 deletions

File tree

server/src/controllers/form.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { FORM_CSS } from '../../../admin/src/form-css';
2+
import { isSafeRedirect } from '../lib/embed';
23

34
const PLUGIN_ID = 'strapi-plugin-form-builder-cms';
45

@@ -74,6 +75,7 @@ function publicPageHtml(form: any): string {
7475
function embedScript(): string {
7576
return `(function () {
7677
var PLUGIN = 'strapi-plugin-form-builder-cms';
78+
var isSafeRedirect = ${isSafeRedirect.toString()};
7779
7880
function init() {
7981
document.querySelectorAll('script[data-form-id]').forEach(function (script) {
@@ -228,11 +230,7 @@ function embedScript(): string {
228230
// redirect after submit, if configured — only to an http(s) or relative URL,
229231
// so a "javascript:"/"data:" value can never run as an open-redirect/XSS.
230232
var redirect = form.settings && form.settings.redirectUrl;
231-
if (redirect) {
232-
var safe = false;
233-
try { var ru = new URL(redirect, window.location.href); safe = ru.protocol === 'http:' || ru.protocol === 'https:'; } catch (e) { safe = false; }
234-
if (safe) { window.location.href = redirect; return; }
235-
}
233+
if (redirect && isSafeRedirect(redirect, window.location.href)) { window.location.href = redirect; return; }
236234
// otherwise show the success message — wrapped in .sfb-form + re-applying the
237235
// theme so it stays styled (the themed <form> we just cleared is gone)
238236
var wrap = document.createElement('div');

server/src/lib/embed.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* Pure helpers for the embed runtime. These are injected into the served embed
3+
* script via `.toString()`, so the browser runs the exact code covered by tests
4+
* here — no drift between the tested function and the shipped one. Keep them
5+
* self-contained (globals only, no imports) so the stringified source runs
6+
* standalone in the browser.
7+
*/
8+
9+
/** Is a post-submit redirect target safe to navigate to (http/https/relative only)? */
10+
export function isSafeRedirect(url: string, base: string): boolean {
11+
try {
12+
const u = new URL(url, base);
13+
return u.protocol === 'http:' || u.protocol === 'https:';
14+
} catch (e) {
15+
return false;
16+
}
17+
}

tests/redirect.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { isSafeRedirect } from '../server/src/lib/embed';
3+
4+
const BASE = 'https://acme.co/contact';
5+
6+
describe('isSafeRedirect (post-submit redirect guard)', () => {
7+
it('allows absolute http(s) URLs', () => {
8+
expect(isSafeRedirect('https://acme.co/thanks', BASE)).toBe(true);
9+
expect(isSafeRedirect('http://acme.co/thanks', BASE)).toBe(true);
10+
});
11+
12+
it('allows relative paths (resolved against the current page)', () => {
13+
expect(isSafeRedirect('/thanks', BASE)).toBe(true);
14+
expect(isSafeRedirect('thank-you', BASE)).toBe(true);
15+
});
16+
17+
it('blocks javascript: and data: URLs (XSS / open-redirect)', () => {
18+
expect(isSafeRedirect('javascript:alert(1)', BASE)).toBe(false);
19+
expect(isSafeRedirect('JavaScript:alert(1)', BASE)).toBe(false);
20+
expect(isSafeRedirect('data:text/html,<script>alert(1)</script>', BASE)).toBe(false);
21+
});
22+
23+
it('blocks other non-http protocols', () => {
24+
expect(isSafeRedirect('mailto:x@y.com', BASE)).toBe(false);
25+
expect(isSafeRedirect('ftp://acme.co/f', BASE)).toBe(false);
26+
expect(isSafeRedirect('tel:+123', BASE)).toBe(false);
27+
});
28+
// note: an empty string is filtered upstream by the `redirect && …` guard in the embed,
29+
// so it never reaches isSafeRedirect (where, against a base, it would resolve to the page).
30+
});

0 commit comments

Comments
 (0)