Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
914184c
[MOL-22453][SX] CI: harden GitHub Actions workflow against injection …
Sep 18, 2026
2a3e2f7
[MOL-22453][SX] Add RegexHelper: shared regex parsing + safe cap, upd…
Sep 18, 2026
5d79c6a
[MOL-22453][SX] sanitize-html: restrict allowedAttributes to safe def…
Sep 18, 2026
0a11d73
[MOL-22453][SX] ButtonField: restrict href to http/https/mailto/tel s…
Sep 18, 2026
10809bc
[MOL-22453][SX] Iframe: validate postMessage origin against iframe sr…
Sep 18, 2026
d22c4fd
[MOL-22453][SX] LocationField: escape search query before use in RegE…
Sep 18, 2026
3beff29
[MOL-22453][SX] StyleHelper: strip @import and url() from schema-auth…
Sep 18, 2026
344ff6e
[MOL-22453][SX] ImageUpload: fix matches rule incorrectly blocking va…
Sep 18, 2026
3509eac
[MOL-22453][SX] FilterCheckbox: omit sanitizeOptions (equivalent to d…
Sep 18, 2026
4d59969
[MOL-22453][SX] yup matches: add comment explaining non-string guard
Sep 18, 2026
f1098b4
[MOL-22453][SX] RegexHelper: rename parseMatchesPattern to compile
Sep 18, 2026
9adca3d
[MOL-22453][SX] RegexHelper: rename MAX_SAFE_PATTERN_INPUT_LENGTH to …
Sep 18, 2026
da72f13
[MOL-22453][SX] yup matches: also pass empty string
Sep 18, 2026
93025b2
[MOL-22453][SX] LocationField: use vm.runInNewContext to prevent CI h…
Sep 18, 2026
6685bec
[MOL-22453][SX] Stories: document url()/import stripping in locationM…
Sep 21, 2026
130e38c
[MOL-22453][SX] Stories: fix curly quotes (U+201C/U+201D) in location…
Sep 21, 2026
7d66156
[MOL-22453][SX] LocationField: increase ReDoS test input to n=30 for …
Sep 21, 2026
5586cd5
[MOL-22453][SX] ImageUpload: use repeat(1000) to exceed MAX_MATCHES_I…
Sep 21, 2026
1e58a5d
[MOL-22453][SX] Tests: fix catastrophic-regex test values to exceed M…
Sep 21, 2026
98d3397
[MOL-22453][SX] Tests: fix masked-field repeat(600) to exceed MAX_MAT…
Sep 21, 2026
c800536
[MOL-22453][SX] Tests: fix masked-field hang tests and max-validation…
Sep 21, 2026
2a481ad
[MOL-22453][SX] Tests: fix v1-incompatible test patterns in masked-fi…
Sep 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .github/workflows/trigger-gitlab-pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,15 @@ jobs:

steps:
- name: Print Configs
env:
HEAD_REF: ${{ github.head_ref }}
REF_NAME: ${{ github.ref_name }}
PR_TITLE: ${{ github.event.pull_request.title }}
HEAD_COMMIT_MSG: ${{ github.event.head_commit.message }}
run: |
[[ $GITHUB_EVENT_NAME = "pull_request" ]] && BRANCH_NAME="${{ github.head_ref }}" || BRANCH_NAME="${{ github.ref_name }}"
[[ $GITHUB_EVENT_NAME = "pull_request" ]] && BRANCH_NAME="$HEAD_REF" || BRANCH_NAME="$REF_NAME"

[[ $GITHUB_EVENT_NAME = "pull_request" ]] && COMMIT_MSG="${{ github.event.pull_request.title }}" || COMMIT_MSG=$(echo -e "${{ github.event.head_commit.message }}" | head -n 1)
[[ $GITHUB_EVENT_NAME = "pull_request" ]] && COMMIT_MSG="$PR_TITLE" || COMMIT_MSG=$(echo -e "$HEAD_COMMIT_MSG" | head -n 1)

PIPELINE_PROJECT_URL="github.com/$GITHUB_REPOSITORY.git"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,15 @@ describe(REFERENCE_KEY, () => {
expect(SUBMIT_FN).toBeCalledWith(expect.objectContaining({ [COMPONENT_ID]: defaultValues }));
});

it("should strip event handler attributes from option labels", () => {
renderComponent({
options: [{ label: '<span onclick="window.xssFired=true">Apple label</span>', value: "Apple" }],
});

const spanElement = screen.getByText("Apple label");
expect(spanElement).not.toHaveAttribute("onclick");
});

it("should be able to render hint", () => {
renderComponent({
label: {
Expand Down
60 changes: 60 additions & 0 deletions src/__tests__/components/custom/iframe/iframe.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,66 @@ describe("iframe", () => {
expect(SUBMIT_FN).toHaveBeenCalledWith(expect.objectContaining({ [COMPONENT_ID]: "hello world" }));
});

describe("postMessage origin validation", () => {
const sendPostMessageFromOrigin = (origin: string, type: EPostMessageEvent, payload?: unknown) => {
fireEvent(window, new MessageEvent("message", { data: { type, payload }, origin }));
};

it("should ignore a setValue postMessage from an origin that does not match src", async () => {
renderComponent({ validationTimeout: -1 });

sendPostMessageFromOrigin("https://attacker.example", EPostMessageEvent.SET_VALUE, "hello world");
await waitFor(() => fireEvent.click(getSubmitButton()));

expect(SUBMIT_FN).toHaveBeenCalledWith({});
});

it("should accept a setValue postMessage from the origin matching src", async () => {
renderComponent({ validationTimeout: -1 });

sendPostMessageFromOrigin(IFRAME_SRC, EPostMessageEvent.SET_VALUE, "hello world");
await waitFor(() => fireEvent.click(getSubmitButton()));

expect(SUBMIT_FN).toHaveBeenCalledWith(expect.objectContaining({ [COMPONENT_ID]: "hello world" }));
});

it("should derive the origin for a relative src (resolved against the current page) and still ignore mismatched origins", async () => {
renderComponent({ src: "/embedded/form", validationTimeout: -1 });

sendPostMessageFromOrigin("https://attacker.example", EPostMessageEvent.SET_VALUE, "hello world");
await waitFor(() => fireEvent.click(getSubmitButton()));

expect(SUBMIT_FN).toHaveBeenCalledWith({});
});

it("should accept a setValue postMessage matching the origin derived from a relative src", async () => {
renderComponent({ src: "/embedded/form", validationTimeout: -1 });

sendPostMessageFromOrigin("http://localhost", EPostMessageEvent.SET_VALUE, "hello world");
await waitFor(() => fireEvent.click(getSubmitButton()));

expect(SUBMIT_FN).toHaveBeenCalledWith(expect.objectContaining({ [COMPONENT_ID]: "hello world" }));
});

it("should derive the origin for a protocol-relative src and still ignore mismatched origins", async () => {
renderComponent({ src: "//localhost/embedded/form", validationTimeout: -1 });

sendPostMessageFromOrigin("https://attacker.example", EPostMessageEvent.SET_VALUE, "hello world");
await waitFor(() => fireEvent.click(getSubmitButton()));

expect(SUBMIT_FN).toHaveBeenCalledWith({});
});

it("should reject every postMessage when src cannot be resolved to a valid http(s) origin", async () => {
renderComponent({ src: "javascript:alert(1)", validationTimeout: -1 });

sendPostMessageFromOrigin("https://attacker.example", EPostMessageEvent.SET_VALUE, "hello world");
await waitFor(() => fireEvent.click(getSubmitButton()));

expect(SUBMIT_FN).toHaveBeenCalledWith({});
});
});

describe("load", () => {
it("should fire a loading event when iframe starts loading", () => {
const testFn = jest.fn();
Expand Down
12 changes: 12 additions & 0 deletions src/__tests__/components/elements/text/text.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,18 @@ describe(UI_TYPE, () => {
expect(screen.getByText("This is a HTML string")).toBeInTheDocument();
});

it("should strip event handler attributes from an otherwise-allowed image tag", () => {
renderComponent({
className: "text-element",
children: '<img src="x" onerror="window.xssFired=true" alt=\'broken image\'>',
});

const imgElement = screen.getByAltText("broken image");
expect(imgElement).toBeInTheDocument();
expect(imgElement).not.toHaveAttribute("onerror");
expect(document.querySelector(".text-element").innerHTML).not.toContain("onerror");
});

it("should be able to sanitize HTML string", () => {
renderComponent({
className: "text-element",
Expand Down
8 changes: 5 additions & 3 deletions src/__tests__/components/fields/button/button.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,11 @@ describe("button", () => {
});

it.each`
scenario | href
${"should not navigate when href is not provided"} | ${undefined}
${"should not navigate when href is invalid"} | ${"invalid-url"}
scenario | href
${"should not navigate when href is not provided"} | ${undefined}
${"should not navigate when href is invalid"} | ${"invalid-url"}
${"should not navigate when href uses javascript: scheme"} | ${"javascript:alert(1)"}
${"should not navigate when href uses data: scheme"} | ${"data:text/html,<script>alert(1)</script>"}
`("$scenario", ({ href }) => {
renderComponent({ overrideButton: { ...(href && { href }) } });
fireEvent.click(getField("button", COMPONENT_LABEL));
Expand Down
Loading
Loading