Compare commits

...

1 Commits

Author SHA1 Message Date
Cursor Agent
6ebc2c4110 fix: validate inputShadow CSS value to prevent parsing errors
- Add sanitizeCssValue function to validate CSS values before DOM insertion
- Reject dangerous patterns (braces, quotes, semicolons, multiple colons)
- Add comprehensive tests for valid and malformed inputShadow values
- Add Zod schema validation to prevent malformed values at data layer
- Fixes FORMBRICKS-RB: SyntaxError when malformed inputShadow contains unexpected colons
2026-03-10 08:47:01 +00:00
3 changed files with 160 additions and 2 deletions

View File

@@ -652,3 +652,109 @@ describe("getBaseProjectStyling_Helper", () => {
expect(styled.isLogoHidden).toBeNull();
});
});
describe("inputShadow validation and sanitization", () => {
beforeEach(() => {
document.head.innerHTML = "";
document.body.innerHTML = "";
setStyleNonce(undefined);
});
afterEach(() => {
const styleElement = document.getElementById("formbricks__css__custom");
if (styleElement) {
styleElement.remove();
}
setStyleNonce(undefined);
});
test("should apply valid inputShadow values", () => {
const validShadows = [
"0 1px 2px 0 rgba(0, 0, 0, 0.05)",
"0 1px 2px 0 rgb(0 0 0 / 0.05)",
"none",
"0 0 10px rgba(0,0,0,0.5)",
"2px 2px 4px #000000",
"inset 0 0 5px rgba(0,0,0,0.1)",
];
validShadows.forEach((shadow) => {
document.head.innerHTML = "";
const styling = getBaseProjectStyling({ brandColor: { light: "#123" }, inputShadow: shadow });
addCustomThemeToDom({ styling });
const styleElement = document.getElementById("formbricks__css__custom") as HTMLStyleElement;
expect(styleElement.innerHTML).toContain("--fb-input-shadow:");
});
});
test("should reject inputShadow with dangerous characters", () => {
const dangerousShadows = [
"0 1px 2px { background: red }",
"0 1px 2px; color: red",
'0 1px 2px "malicious"',
"0 1px 2px 'malicious'",
"data::image/svg+xml",
"url(javascript:alert(1))",
];
dangerousShadows.forEach((shadow) => {
document.head.innerHTML = "";
const styling = getBaseProjectStyling({ brandColor: { light: "#123" }, inputShadow: shadow });
addCustomThemeToDom({ styling });
const styleElement = document.getElementById("formbricks__css__custom") as HTMLStyleElement;
expect(styleElement.innerHTML).not.toContain("--fb-input-shadow:");
});
});
test("should handle null and undefined inputShadow gracefully", () => {
const styling1 = getBaseProjectStyling({ brandColor: { light: "#123" }, inputShadow: null });
addCustomThemeToDom({ styling: styling1 });
let styleElement = document.getElementById("formbricks__css__custom") as HTMLStyleElement;
expect(styleElement.innerHTML).not.toContain("--fb-input-shadow:");
document.head.innerHTML = "";
const styling2 = getBaseProjectStyling({ brandColor: { light: "#123" }, inputShadow: undefined });
addCustomThemeToDom({ styling: styling2 });
styleElement = document.getElementById("formbricks__css__custom") as HTMLStyleElement;
expect(styleElement.innerHTML).not.toContain("--fb-input-shadow:");
});
test("should reject empty or whitespace-only inputShadow", () => {
const emptyShadows = ["", " ", "\t", "\n"];
emptyShadows.forEach((shadow) => {
document.head.innerHTML = "";
const styling = getBaseProjectStyling({ brandColor: { light: "#123" }, inputShadow: shadow });
addCustomThemeToDom({ styling });
const styleElement = document.getElementById("formbricks__css__custom") as HTMLStyleElement;
expect(styleElement.innerHTML).not.toContain("--fb-input-shadow:");
});
});
test("should not throw error when setting innerHTML with valid shadow", () => {
const styling = getBaseProjectStyling({
brandColor: { light: "#123" },
inputShadow: "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
});
expect(() => {
addCustomThemeToDom({ styling });
}).not.toThrow();
const styleElement = document.getElementById("formbricks__css__custom") as HTMLStyleElement;
expect(styleElement).not.toBeNull();
expect(styleElement.innerHTML).toBeTruthy();
});
test("should prevent CSS injection through malformed colon patterns", () => {
const malformedShadows = ["test::malformed", ":::invalid", "0px::1px::2px", "data::base64::encoded"];
malformedShadows.forEach((shadow) => {
document.head.innerHTML = "";
const styling = getBaseProjectStyling({ brandColor: { light: "#123" }, inputShadow: shadow });
addCustomThemeToDom({ styling });
const styleElement = document.getElementById("formbricks__css__custom") as HTMLStyleElement;
expect(styleElement.innerHTML).not.toContain("--fb-input-shadow:");
});
});
});

View File

@@ -55,6 +55,38 @@ export const addStylesToDom = () => {
}
};
/**
* Validates and sanitizes a CSS value to prevent CSS injection and parsing errors.
* Returns null if the value is invalid or potentially dangerous.
*/
const sanitizeCssValue = (value: string): string | null => {
if (!value || typeof value !== "string") return null;
const trimmedValue = value.trim();
if (trimmedValue === "") return null;
// Check for dangerous patterns that could break CSS parsing
// Reject values containing unescaped braces, quotes, or semicolons that could break out of the CSS context
if (/[{};"']/.test(trimmedValue)) {
return null;
}
// Check for multiple consecutive colons (::) which might indicate malformed data URIs or other issues
if (/:{2,}/.test(trimmedValue)) {
return null;
}
// For box-shadow values, validate basic structure
// Valid patterns include: "none", or combinations of: offset-x offset-y [blur-radius] [spread-radius] [color] [inset]
// Allow common CSS units and color formats
const validCssPattern = /^[\w\s\-.,()/#%]+$/;
if (!validCssPattern.test(trimmedValue)) {
return null;
}
return trimmedValue;
};
export const addCustomThemeToDom = ({ styling }: { styling: TProjectStyling | TSurveyStyling }): void => {
// Check if the style element already exists
let styleElement = document.getElementById("formbricks__css__custom") as HTMLStyleElement | null;
@@ -233,7 +265,10 @@ export const addCustomThemeToDom = ({ styling }: { styling: TProjectStyling | TS
appendCssVariable("input-padding-y", formatDimension(styling.inputPaddingY));
if (styling.inputPlaceholderOpacity !== undefined)
appendCssVariable("input-placeholder-opacity", `${styling.inputPlaceholderOpacity}`);
appendCssVariable("input-shadow", styling.inputShadow);
if (styling.inputShadow !== undefined && styling.inputShadow !== null) {
const sanitizedShadow = sanitizeCssValue(styling.inputShadow);
appendCssVariable("input-shadow", sanitizedShadow);
}
// Options (Advanced)
appendCssVariable("option-bg-color", styling.optionBgColor?.light ?? styling.inputColor?.light);

View File

@@ -68,7 +68,24 @@ export const ZBaseStyling = z.object({
inputPlaceholderOpacity: z.number().max(1).min(0).nullish(),
inputPaddingX: z.union([z.number(), z.string()]).nullish(),
inputPaddingY: z.union([z.number(), z.string()]).nullish(),
inputShadow: z.string().nullish(),
inputShadow: z
.string()
.refine(
(val) => {
if (!val) return true;
// Reject values with dangerous characters that could break CSS parsing
if (/[{};"']/.test(val)) return false;
// Reject values with multiple consecutive colons
if (/:{2,}/.test(val)) return false;
// Validate basic CSS pattern
if (!/^[\w\s\-.,()/#%]+$/.test(val)) return false;
return true;
},
{
message: "Invalid CSS shadow value",
}
)
.nullish(),
// Options
optionBgColor: ZStylingColor.nullish(),