diff --git a/.github/workflows/trigger-gitlab-pipeline.yml b/.github/workflows/trigger-gitlab-pipeline.yml index c7d934fac7..94f397cae9 100644 --- a/.github/workflows/trigger-gitlab-pipeline.yml +++ b/.github/workflows/trigger-gitlab-pipeline.yml @@ -14,10 +14,13 @@ jobs: steps: - name: Print Configs + env: + HEAD_COMMIT_MSG: ${{ github.event.head_commit.message }} + REF_NAME: ${{ github.ref_name }} run: | - BRANCH_NAME="${{ github.ref_name }}" + BRANCH_NAME="$REF_NAME" - COMMIT_MSG=$(echo -e "${{ github.event.head_commit.message }}" | head -n 1) + COMMIT_MSG=$(printf '%s\n' "$HEAD_COMMIT_MSG" | awk 'NR>1{exit};1') PIPELINE_PROJECT_URL="github.com/$GITHUB_REPOSITORY.git" diff --git a/codemods/run-codemod.ts b/codemods/run-codemod.ts index c00feef92b..af8008bd60 100644 --- a/codemods/run-codemod.ts +++ b/codemods/run-codemod.ts @@ -1,5 +1,5 @@ import { checkbox, confirm, input, select } from "@inquirer/prompts"; -import { execSync } from "child_process"; +import { execFileSync } from "child_process"; import * as fs from "fs"; import * as path from "path"; @@ -54,14 +54,23 @@ function runCodemods(selection: UserSelection): void { selectedCodemods.forEach((codemod) => { const codemodPath = path.join(codemodsDir, codemod, "index.ts"); - let command = `npx --yes jscodeshift --parser=tsx -t ${codemodPath} ${targetPath}`; - console.log( `Running codemod: ${codemod} on target path: ${targetPath}` ); try { - execSync(command, { stdio: "inherit" }); + execFileSync( + "npx", + [ + "--yes", + "jscodeshift", + "--parser=tsx", + "-t", + codemodPath, + targetPath, + ], + { stdio: "inherit" } + ); console.log( `Codemod ${codemod} executed successfully on ${targetPath}` ); diff --git a/src/notification-banner/notification-banner-hoc.tsx b/src/notification-banner/notification-banner-hoc.tsx index d2da66346e..1a72cc7653 100644 --- a/src/notification-banner/notification-banner-hoc.tsx +++ b/src/notification-banner/notification-banner-hoc.tsx @@ -9,6 +9,21 @@ import type { NotificationContentAttributes, } from "./types"; +function sanitizeLinkAttributes( + attrs: ContentLinkAttributes +): ContentLinkAttributes { + const { href, ...rest } = attrs; + + if ( + typeof href !== "string" || + !DOMPurify.isValidAttribute("a", "href", href) + ) { + return rest; + } + + return attrs; +} + /** * Higher-order component that wraps `NotificationBanner` and renders its * content from a structured data array. @@ -42,8 +57,10 @@ export const withNotificationBanner = ( /> ); } else { - const otherAttributes = - attribute.otherAttributes as ContentLinkAttributes; + const otherAttributes = sanitizeLinkAttributes( + (attribute.otherAttributes ?? + {}) as ContentLinkAttributes + ); return ( { expect(mockOnClick).toHaveBeenCalledTimes(1); }); + it("should strip javascript: href from link otherAttributes", () => { + const HOCElement = withNotificationBanner([ + { + type: "link", + content: "malicious link", + otherAttributes: { + href: "javascript:alert(document.cookie)", + }, + }, + ]); + render(); + + const anchor = document.querySelector("a"); + expect(anchor).toBeInTheDocument(); + expect(anchor).not.toHaveAttribute("href"); + }); + + it("should strip data: href from link otherAttributes", () => { + const HOCElement = withNotificationBanner([ + { + type: "link", + content: "data link", + otherAttributes: { + href: "data:text/html,", + }, + }, + ]); + render(); + + const anchor = document.querySelector("a"); + expect(anchor).toBeInTheDocument(); + expect(anchor).not.toHaveAttribute("href"); + }); + + it("should preserve safe href schemes in link otherAttributes", () => { + const HOCElement = withNotificationBanner([ + { + type: "link", + content: "safe link", + otherAttributes: { + href: "https://www.example.com", + }, + }, + ]); + render(); + + const anchor = document.querySelector("a"); + expect(anchor).toBeInTheDocument(); + expect(anchor).toHaveAttribute("href", "https://www.example.com"); + }); + it("should sanitise the content", () => { const HOCElement = withNotificationBanner([ {