mirror of
https://github.com/formbricks/formbricks.git
synced 2026-03-13 01:58:40 -05:00
Compare commits
1 Commits
cursor/cus
...
cursor/inp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ebc2c4110 |
@@ -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:");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user