From b33b63a5b4a3589b952ee2a35ee48e952705ab79 Mon Sep 17 00:00:00 2001 From: fairlighteth <31534717+fairlighteth@users.noreply.github.com> Date: Fri, 5 Jun 2026 19:36:24 +0100 Subject: [PATCH 1/5] fix: harden release workflows and widget snippets - validate release tags against main before secret-bearing jobs run - sanitize widget embed params to block script and tradeType injection --- .github/workflows/deployment-v2.yml | 25 +++++-- .github/workflows/deployment.yml | 68 ++++++++++++++++++- .github/workflows/vercel.yml | 6 ++ .../utils/formatParameters.test.ts | 68 +++++++++++++++++++ .../app/embedDialog/utils/formatParameters.ts | 57 +++++++++++----- .../embedDialog/utils/sanitizeParameters.ts | 23 ++++++- 6 files changed, 222 insertions(+), 25 deletions(-) create mode 100644 apps/widget-configurator/src/app/embedDialog/utils/formatParameters.test.ts diff --git a/.github/workflows/deployment-v2.yml b/.github/workflows/deployment-v2.yml index bd924fba380..5de682f5233 100644 --- a/.github/workflows/deployment-v2.yml +++ b/.github/workflows/deployment-v2.yml @@ -19,11 +19,13 @@ jobs: if: startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest outputs: + release-commit: ${{ steps.collect.outputs.release-commit }} release-tags: ${{ steps.collect.outputs.release-tags }} steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: + ref: refs/heads/main fetch-depth: 0 persist-credentials: false @@ -32,9 +34,15 @@ jobs: run: | set -euo pipefail - git fetch --tags origin + git fetch --tags origin main release_commit="$(git rev-list -n 1 "${GITHUB_REF}")" + + if ! git merge-base --is-ancestor "${release_commit}" "origin/main"; then + echo "::error::Release tag ${GITHUB_REF_NAME} does not point to a commit reachable from origin/main" + exit 1 + fi + release_tags="$( git tag --points-at "${release_commit}" \ | sort \ @@ -45,6 +53,7 @@ jobs: release_tags="${GITHUB_REF_NAME}" fi + echo "release-commit=${release_commit}" >> "$GITHUB_OUTPUT" echo "release-tags=${release_tags}" >> "$GITHUB_OUTPUT" sync-develop: @@ -61,6 +70,7 @@ jobs: - name: Checkout workflow repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: + ref: refs/heads/main fetch-depth: 0 persist-credentials: false @@ -148,10 +158,12 @@ jobs: run: | set -euo pipefail - git fetch origin main staging + git fetch origin main staging --tags git checkout -B staging origin/staging - git merge --ff-only origin/main + git merge --ff-only "${RELEASE_COMMIT}" git push origin staging + env: + RELEASE_COMMIT: ${{ needs.collect-release-metadata.outputs.release-commit }} notify-production-approval: name: Notify Slack for production approval @@ -181,6 +193,7 @@ jobs: - name: Checkout workflow repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: + ref: refs/heads/main fetch-depth: 0 persist-credentials: false - name: Setup release sync @@ -196,7 +209,9 @@ jobs: run: | set -euo pipefail - git fetch origin main production + git fetch origin main production --tags git checkout -B production origin/production - git merge --ff-only origin/main + git merge --ff-only "${RELEASE_COMMIT}" git push origin production + env: + RELEASE_COMMIT: ${{ needs.collect-release-metadata.outputs.release-commit }} diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 847bffac526..09ac5b21a68 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -16,7 +16,39 @@ on: - EXPLORER - ALL +permissions: + contents: read + jobs: + validate-release-tag: + name: Validate release tag + if: startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + outputs: + release-commit: ${{ steps.validate.outputs.release-commit }} + steps: + - name: Checkout trusted workflow repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: refs/heads/main + fetch-depth: 0 + persist-credentials: false + + - name: Validate release commit + id: validate + run: | + set -euo pipefail + + git fetch origin main --tags + + release_commit="$(git rev-list -n 1 "${GITHUB_REF}")" + + if ! git merge-base --is-ancestor "${release_commit}" "origin/main"; then + echo "::error::Release tag ${GITHUB_REF_NAME} does not point to a commit reachable from origin/main" + exit 1 + fi + + echo "release-commit=${release_commit}" >> "$GITHUB_OUTPUT" vercel-dev: # Deploys to Vercel dev environment @@ -35,8 +67,41 @@ jobs: # Deploys to Vercel staging environment only when there is a tag for CowSwap or Explorer name: Vercel pre-prod if: startsWith(github.ref, 'refs/tags') + needs: validate-release-tag uses: ./.github/workflows/vercel.yml - secrets: inherit + secrets: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID_COWSWAP: ${{ secrets.VERCEL_PROJECT_ID_COWSWAP }} + VERCEL_PROJECT_ID_EXPLORER: ${{ secrets.VERCEL_PROJECT_ID_EXPLORER }} + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + REACT_APP_PINATA_API_KEY: ${{ secrets.REACT_APP_PINATA_API_KEY }} + REACT_APP_PINATA_SECRET_API_KEY: ${{ secrets.REACT_APP_PINATA_SECRET_API_KEY }} + REACT_APP_BLOCKNATIVE_API_KEY: ${{ secrets.REACT_APP_BLOCKNATIVE_API_KEY }} + REACT_APP_GOOGLE_ANALYTICS_ID: ${{ secrets.REACT_APP_GOOGLE_ANALYTICS_ID }} + REACT_APP_LAUNCH_DARKLY_KEY: ${{ secrets.REACT_APP_LAUNCH_DARKLY_KEY }} + REACT_APP_INFURA_KEY: ${{ secrets.REACT_APP_INFURA_KEY }} + REACT_APP_NETWORK_URL_1: ${{ secrets.REACT_APP_NETWORK_URL_1 }} + REACT_APP_NETWORK_URL_56: ${{ secrets.REACT_APP_NETWORK_URL_56 }} + REACT_APP_NETWORK_URL_100: ${{ secrets.REACT_APP_NETWORK_URL_100 }} + REACT_APP_NETWORK_URL_137: ${{ secrets.REACT_APP_NETWORK_URL_137 }} + REACT_APP_NETWORK_URL_8453: ${{ secrets.REACT_APP_NETWORK_URL_8453 }} + REACT_APP_NETWORK_URL_9745: ${{ secrets.REACT_APP_NETWORK_URL_9745 }} + REACT_APP_NETWORK_URL_42161: ${{ secrets.REACT_APP_NETWORK_URL_42161 }} + REACT_APP_NETWORK_URL_43114: ${{ secrets.REACT_APP_NETWORK_URL_43114 }} + REACT_APP_NETWORK_URL_57073: ${{ secrets.REACT_APP_NETWORK_URL_57073 }} + REACT_APP_NETWORK_URL_59144: ${{ secrets.REACT_APP_NETWORK_URL_59144 }} + REACT_APP_NETWORK_URL_11155111: ${{ secrets.REACT_APP_NETWORK_URL_11155111 }} + REACT_APP_WC_PROJECT_ID: ${{ secrets.REACT_APP_WC_PROJECT_ID }} + REACT_APP_IPFS_READ_URI: ${{ secrets.REACT_APP_IPFS_READ_URI }} + EXPLORER_SENTRY_DSN: ${{ secrets.EXPLORER_SENTRY_DSN }} + REACT_APP_SUBGRAPH_URL_MAINNET: ${{ secrets.REACT_APP_SUBGRAPH_URL_MAINNET }} + REACT_APP_SUBGRAPH_URL_ARBITRUM_ONE: ${{ secrets.REACT_APP_SUBGRAPH_URL_ARBITRUM_ONE }} + REACT_APP_SUBGRAPH_URL_BASE: ${{ secrets.REACT_APP_SUBGRAPH_URL_BASE }} + REACT_APP_SUBGRAPH_URL_GNOSIS_CHAIN: ${{ secrets.REACT_APP_SUBGRAPH_URL_GNOSIS_CHAIN }} + BFF_BASE_URL: ${{ secrets.BFF_BASE_URL }} + CMS_BASE_URL: ${{ secrets.CMS_BASE_URL }} + REACT_APP_NEAR_API_KEY: ${{ secrets.REACT_APP_NEAR_API_KEY }} strategy: matrix: env_name: [ staging ] # deploys both in parallel @@ -44,6 +109,7 @@ jobs: env_name: ${{ matrix.env_name }} # Pick app according to published tag app: ${{ startsWith(github.ref, 'refs/tags/explorer') && 'EXPLORER' || 'COWSWAP' }} + checkout_ref: ${{ needs.validate-release-tag.outputs.release-commit }} disable_nx_cache: true vercel-prod: diff --git a/.github/workflows/vercel.yml b/.github/workflows/vercel.yml index 421516b6347..dfa29caaa5e 100644 --- a/.github/workflows/vercel.yml +++ b/.github/workflows/vercel.yml @@ -11,6 +11,11 @@ on: description: 'Application to deploy. Options are: COWSWAP, EXPLORER' required: true type: string + checkout_ref: + description: 'Exact git ref or commit to check out before building' + required: false + type: string + default: '' disable_nx_cache: description: 'Disable NX cache' required: false @@ -34,6 +39,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false + ref: ${{ inputs.checkout_ref != '' && inputs.checkout_ref || github.sha }} - name: Install pnpm uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 diff --git a/apps/widget-configurator/src/app/embedDialog/utils/formatParameters.test.ts b/apps/widget-configurator/src/app/embedDialog/utils/formatParameters.test.ts new file mode 100644 index 00000000000..cdcdd965278 --- /dev/null +++ b/apps/widget-configurator/src/app/embedDialog/utils/formatParameters.test.ts @@ -0,0 +1,68 @@ +import { CowSwapWidgetParams, TradeType } from '@cowprotocol/widget-lib' + +import { vanillaNoDepsExample } from './htmlExample' +import { jsExample } from './jsExample' +import { tsExample } from './tsExample' + +import { ColorPalette } from '../../configurator/types' + +const defaultPalette: ColorPalette = { + primary: '#000000', + background: '#111111', + paper: '#222222', + text: '#ffffff', +} + +describe('widget snippet serialization', () => { + it('escapes script-breaking token values in the HTML snippet', () => { + const params: CowSwapWidgetParams = { + appCode: 'Widget App', + sell: { + asset: '', + }, + } + + const snippet = vanillaNoDepsExample(params, defaultPalette) + + expect(snippet).not.toContain('') + expect(snippet).toContain('\\u003c/script\\u003e\\u003cscript\\u003ealert(1)\\u003c/script\\u003e') + }) + + it('drops invalid tradeType values from Javascript snippets', () => { + const params = { + appCode: 'Widget App', + tradeType: 'swap";alert(1);//', + } as unknown as CowSwapWidgetParams + + const snippet = jsExample(params, defaultPalette) + + expect(snippet).not.toContain('tradeType') + expect(snippet).not.toContain('alert(1)') + }) + + it('emits TradeType enums only for valid Typescript trade types', () => { + const validParams: CowSwapWidgetParams = { + appCode: 'Widget App', + tradeType: TradeType.ADVANCED, + enabledTradeTypes: [TradeType.SWAP, TradeType.YIELD], + } + + const validSnippet = tsExample(validParams, defaultPalette) + + expect(validSnippet).toContain('TradeType.ADVANCED') + expect(validSnippet).toContain('TradeType.SWAP') + expect(validSnippet).toContain('TradeType.YIELD') + + const invalidParams = { + appCode: 'Widget App', + tradeType: 'advanced};alert(1);//', + enabledTradeTypes: [TradeType.SWAP, 'yield);alert(1);//'], + } as unknown as CowSwapWidgetParams + + const invalidSnippet = tsExample(invalidParams, defaultPalette) + + expect(invalidSnippet).not.toContain('alert(1)') + expect(invalidSnippet).not.toContain('TradeType.ADVANCED};ALERT(1);//') + expect(invalidSnippet).toContain('TradeType.SWAP') + }) +}) diff --git a/apps/widget-configurator/src/app/embedDialog/utils/formatParameters.ts b/apps/widget-configurator/src/app/embedDialog/utils/formatParameters.ts index b37dbbe7734..5737e9101ce 100644 --- a/apps/widget-configurator/src/app/embedDialog/utils/formatParameters.ts +++ b/apps/widget-configurator/src/app/embedDialog/utils/formatParameters.ts @@ -1,10 +1,12 @@ -import { CowSwapWidgetParams } from '@cowprotocol/widget-lib' +import { CowSwapWidgetParams, TradeType } from '@cowprotocol/widget-lib' import { sanitizeParameters } from './sanitizeParameters' import { ColorPalette } from '../../configurator/types' import { COMMENTS_BY_PARAM_NAME, COMMENTS_BY_PARAM_NAME_TYPESCRIPT, REMOVE_PARAMS } from '../const' +const TRADE_TYPE_PLACEHOLDER_PREFIX = '__COW_WIDGET_TRADE_TYPE__' + export function formatParameters( params: CowSwapWidgetParams, padLeft = 0, @@ -16,8 +18,10 @@ export function formatParameters( delete paramsSanitized[propName] }) + const paramsForSerialization = isTypescript ? replaceTradeTypesWithPlaceholders(paramsSanitized) : paramsSanitized + // Stringify params - const formattedParams = JSON.stringify(paramsSanitized, null, 4) + const formattedParams = escapeScriptSensitiveJson(JSON.stringify(paramsForSerialization, null, 4)) // Add comments const commentsByParamName = isTypescript @@ -28,21 +32,7 @@ export function formatParameters( return acc.replace(new RegExp(`"${propName}".*$`, 'gm'), `$& // ${commentsByParamName[propName]}`) }, formattedParams) - // Add values - const tradeTypeValue = isTypescript ? 'TradeType.' + params.tradeType?.toUpperCase() : `"${params.tradeType}"` - const valuesByParamName: Record = tradeTypeValue ? { tradeType: tradeTypeValue } : {} - - let resultWithValues = Object.keys(valuesByParamName).reduce((acc, propName) => { - return acc.replace(new RegExp(`("${propName}".* )(".*")(.*)$`, 'gm'), `$1${valuesByParamName[propName]}$3`) - }, resultWithComments) - - // Fix the enabledTradeTypes - if (isTypescript) { - resultWithValues = resultWithValues.replace( - new RegExp(/^(\s*)"(\w*)"(,?)$/gm), - (_match, space, tradeType, comma) => space + 'TradeType.' + tradeType.toUpperCase() + comma, - ) - } + const resultWithValues = isTypescript ? replaceTradeTypePlaceholders(resultWithComments) : resultWithComments if (padLeft === 0) { return resultWithValues @@ -59,3 +49,36 @@ export function formatParameters( .join('\n') ) } + +function escapeScriptSensitiveJson(value: string): string { + return value + .replace(//g, '\\u003e') + .replace(/&/g, '\\u0026') + .replace(/\u2028/g, '\\u2028') + .replace(/\u2029/g, '\\u2029') +} + +function replaceTradeTypesWithPlaceholders(params: CowSwapWidgetParams): CowSwapWidgetParams { + return { + ...params, + ...(params.tradeType ? { tradeType: toTradeTypePlaceholder(params.tradeType) as TradeType } : {}), + ...(params.enabledTradeTypes + ? { + enabledTradeTypes: params.enabledTradeTypes.map( + (tradeType) => toTradeTypePlaceholder(tradeType) as TradeType, + ), + } + : {}), + } +} + +function replaceTradeTypePlaceholders(value: string): string { + return value.replace(/"__COW_WIDGET_TRADE_TYPE__(swap|limit|advanced|yield)"/g, (_match, tradeType) => { + return `TradeType.${tradeType.toUpperCase()}` + }) +} + +function toTradeTypePlaceholder(tradeType: TradeType): string { + return `${TRADE_TYPE_PLACEHOLDER_PREFIX}${tradeType}` +} diff --git a/apps/widget-configurator/src/app/embedDialog/utils/sanitizeParameters.ts b/apps/widget-configurator/src/app/embedDialog/utils/sanitizeParameters.ts index 1d7133d04f9..740f5dcc97b 100644 --- a/apps/widget-configurator/src/app/embedDialog/utils/sanitizeParameters.ts +++ b/apps/widget-configurator/src/app/embedDialog/utils/sanitizeParameters.ts @@ -1,4 +1,9 @@ -import { CowSwapWidgetPalette, CowSwapWidgetPaletteColors, CowSwapWidgetParams } from '@cowprotocol/widget-lib' +import { + CowSwapWidgetPalette, + CowSwapWidgetPaletteColors, + CowSwapWidgetParams, + TradeType, +} from '@cowprotocol/widget-lib' import { ColorPalette } from '../../configurator/types' import { SANITIZE_PARAMS } from '../const' @@ -6,11 +11,21 @@ import { SANITIZE_PARAMS } from '../const' // TODO: Add proper return type annotation // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function sanitizeParameters(params: CowSwapWidgetParams, defaultPalette: ColorPalette) { - return { + const sanitized: CowSwapWidgetParams = { ...params, ...SANITIZE_PARAMS, theme: sanitizePalette(params, defaultPalette), } + + if (sanitized.tradeType && !isTradeType(sanitized.tradeType)) { + delete sanitized.tradeType + } + + if (Array.isArray(sanitized.enabledTradeTypes)) { + sanitized.enabledTradeTypes = sanitized.enabledTradeTypes.filter(isTradeType) + } + + return sanitized } // Keep only changed values @@ -33,3 +48,7 @@ function sanitizePalette(params: CowSwapWidgetParams, defaultPalette: ColorPalet return paletteDiff } + +function isTradeType(value: unknown): value is TradeType { + return Object.values(TradeType).includes(value as TradeType) +} From f199dc5a39cc2eb188442eeae985d9a939beb364 Mon Sep 17 00:00:00 2001 From: fairlighteth <31534717+fairlighteth@users.noreply.github.com> Date: Fri, 5 Jun 2026 19:49:37 +0100 Subject: [PATCH 2/5] fix: close remaining release-tag workflow gaps - load the staging sync path from a trusted ref before using app creds - call the tag-triggered Vercel reusable workflow from main instead of the tag workspace --- .github/workflows/deployment-v2.yml | 9 +++++---- .github/workflows/deployment.yml | 12 ++++++------ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.github/workflows/deployment-v2.yml b/.github/workflows/deployment-v2.yml index 5de682f5233..9beb5be3707 100644 --- a/.github/workflows/deployment-v2.yml +++ b/.github/workflows/deployment-v2.yml @@ -2,8 +2,8 @@ name: Deployment v2 on: push: - branches: [ main ] - tags: [ cowswap-*, explorer-* ] + branches: [main] + tags: [cowswap-*, explorer-*] concurrency: group: release-branch-sync @@ -142,6 +142,7 @@ jobs: - name: Checkout workflow repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: + ref: refs/heads/main fetch-depth: 0 persist-credentials: false @@ -168,7 +169,7 @@ jobs: notify-production-approval: name: Notify Slack for production approval if: startsWith(github.ref, 'refs/tags/cowswap-') || startsWith(github.ref, 'refs/tags/explorer-') - needs: [ collect-release-metadata, sync-staging ] + needs: [collect-release-metadata, sync-staging] runs-on: ubuntu-latest steps: - name: Notify Slack @@ -186,7 +187,7 @@ jobs: sync-production: name: Fast-forward production to main if: startsWith(github.ref, 'refs/tags/cowswap-') || startsWith(github.ref, 'refs/tags/explorer-') - needs: [ collect-release-metadata, notify-production-approval ] + needs: [collect-release-metadata, notify-production-approval] runs-on: ubuntu-latest environment: production # Env configured in GitHub UI. Requires manual approval before this job can run steps: diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 09ac5b21a68..3849e0cd63e 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -3,8 +3,8 @@ name: Deployment on: # build when pushing to main/develop, or create a release push: - branches: [ main, develop ] - tags: [ cowswap-v*, explorer-v* ] + branches: [main, develop] + tags: [cowswap-v*, explorer-v*] workflow_dispatch: # Manually trigger it via UI/CLI/API inputs: app: @@ -58,7 +58,7 @@ jobs: secrets: inherit strategy: matrix: - app: [ EXPLORER, COWSWAP ] + app: [EXPLORER, COWSWAP] with: env_name: dev app: ${{ matrix.app }} @@ -68,7 +68,7 @@ jobs: name: Vercel pre-prod if: startsWith(github.ref, 'refs/tags') needs: validate-release-tag - uses: ./.github/workflows/vercel.yml + uses: cowprotocol/cowswap/.github/workflows/vercel.yml@main secrets: VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} VERCEL_PROJECT_ID_COWSWAP: ${{ secrets.VERCEL_PROJECT_ID_COWSWAP }} @@ -104,7 +104,7 @@ jobs: REACT_APP_NEAR_API_KEY: ${{ secrets.REACT_APP_NEAR_API_KEY }} strategy: matrix: - env_name: [ staging ] # deploys both in parallel + env_name: [staging] # deploys both in parallel with: env_name: ${{ matrix.env_name }} # Pick app according to published tag @@ -128,7 +128,7 @@ jobs: notify-failure: name: Notify Slack on Failure - needs: [ vercel-dev, vercel-pre-prod, vercel-prod ] + needs: [vercel-dev, vercel-pre-prod, vercel-prod] runs-on: ubuntu-latest if: failure() && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') From 3e9c2c882794f8fbb2f2da48acf604f9700afb44 Mon Sep 17 00:00:00 2001 From: fairlighteth <31534717+fairlighteth@users.noreply.github.com> Date: Fri, 5 Jun 2026 20:30:34 +0100 Subject: [PATCH 3/5] fix: normalize malformed widget trade types --- .../utils/formatParameters.test.ts | 19 +++++++++++++++++++ .../embedDialog/utils/sanitizeParameters.ts | 10 +++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/apps/widget-configurator/src/app/embedDialog/utils/formatParameters.test.ts b/apps/widget-configurator/src/app/embedDialog/utils/formatParameters.test.ts index cdcdd965278..d5ddd0a94f0 100644 --- a/apps/widget-configurator/src/app/embedDialog/utils/formatParameters.test.ts +++ b/apps/widget-configurator/src/app/embedDialog/utils/formatParameters.test.ts @@ -65,4 +65,23 @@ describe('widget snippet serialization', () => { expect(invalidSnippet).not.toContain('TradeType.ADVANCED};ALERT(1);//') expect(invalidSnippet).toContain('TradeType.SWAP') }) + + it('handles malformed trade type shapes safely', () => { + const malformedParams = { + appCode: 'Widget App', + tradeType: '', + enabledTradeTypes: 'swap', + } as unknown as CowSwapWidgetParams + + expect(() => jsExample(malformedParams, defaultPalette)).not.toThrow() + expect(() => tsExample(malformedParams, defaultPalette)).not.toThrow() + + const jsSnippet = jsExample(malformedParams, defaultPalette) + const tsSnippet = tsExample(malformedParams, defaultPalette) + + expect(jsSnippet).not.toContain('tradeType') + expect(jsSnippet).not.toContain('enabledTradeTypes') + expect(tsSnippet).not.toContain('tradeType') + expect(tsSnippet).not.toContain('enabledTradeTypes') + }) }) diff --git a/apps/widget-configurator/src/app/embedDialog/utils/sanitizeParameters.ts b/apps/widget-configurator/src/app/embedDialog/utils/sanitizeParameters.ts index 740f5dcc97b..cbe672357af 100644 --- a/apps/widget-configurator/src/app/embedDialog/utils/sanitizeParameters.ts +++ b/apps/widget-configurator/src/app/embedDialog/utils/sanitizeParameters.ts @@ -17,12 +17,16 @@ export function sanitizeParameters(params: CowSwapWidgetParams, defaultPalette: theme: sanitizePalette(params, defaultPalette), } - if (sanitized.tradeType && !isTradeType(sanitized.tradeType)) { + if (!isTradeType(sanitized.tradeType)) { delete sanitized.tradeType } - if (Array.isArray(sanitized.enabledTradeTypes)) { - sanitized.enabledTradeTypes = sanitized.enabledTradeTypes.filter(isTradeType) + if (sanitized.enabledTradeTypes !== undefined) { + if (Array.isArray(sanitized.enabledTradeTypes)) { + sanitized.enabledTradeTypes = sanitized.enabledTradeTypes.filter(isTradeType) + } else { + delete sanitized.enabledTradeTypes + } } return sanitized From ec41df56cd5e58b3f45320f52ba11bd7b26d4e1e Mon Sep 17 00:00:00 2001 From: fairlighteth <31534717+fairlighteth@users.noreply.github.com> Date: Tue, 16 Jun 2026 09:26:49 +0100 Subject: [PATCH 4/5] chore: narrow PR 7616 workflow scope - drop legacy deployment.yml hardening from this PR - remove the now-unused vercel workflow ref override - keep the release-tag changes focused on deployment-v2 --- .github/workflows/deployment.yml | 80 +++----------------------------- .github/workflows/vercel.yml | 6 --- 2 files changed, 7 insertions(+), 79 deletions(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 3849e0cd63e..847bffac526 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -3,8 +3,8 @@ name: Deployment on: # build when pushing to main/develop, or create a release push: - branches: [main, develop] - tags: [cowswap-v*, explorer-v*] + branches: [ main, develop ] + tags: [ cowswap-v*, explorer-v* ] workflow_dispatch: # Manually trigger it via UI/CLI/API inputs: app: @@ -16,39 +16,7 @@ on: - EXPLORER - ALL -permissions: - contents: read - jobs: - validate-release-tag: - name: Validate release tag - if: startsWith(github.ref, 'refs/tags/') - runs-on: ubuntu-latest - outputs: - release-commit: ${{ steps.validate.outputs.release-commit }} - steps: - - name: Checkout trusted workflow repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: refs/heads/main - fetch-depth: 0 - persist-credentials: false - - - name: Validate release commit - id: validate - run: | - set -euo pipefail - - git fetch origin main --tags - - release_commit="$(git rev-list -n 1 "${GITHUB_REF}")" - - if ! git merge-base --is-ancestor "${release_commit}" "origin/main"; then - echo "::error::Release tag ${GITHUB_REF_NAME} does not point to a commit reachable from origin/main" - exit 1 - fi - - echo "release-commit=${release_commit}" >> "$GITHUB_OUTPUT" vercel-dev: # Deploys to Vercel dev environment @@ -58,7 +26,7 @@ jobs: secrets: inherit strategy: matrix: - app: [EXPLORER, COWSWAP] + app: [ EXPLORER, COWSWAP ] with: env_name: dev app: ${{ matrix.app }} @@ -67,49 +35,15 @@ jobs: # Deploys to Vercel staging environment only when there is a tag for CowSwap or Explorer name: Vercel pre-prod if: startsWith(github.ref, 'refs/tags') - needs: validate-release-tag - uses: cowprotocol/cowswap/.github/workflows/vercel.yml@main - secrets: - VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} - VERCEL_PROJECT_ID_COWSWAP: ${{ secrets.VERCEL_PROJECT_ID_COWSWAP }} - VERCEL_PROJECT_ID_EXPLORER: ${{ secrets.VERCEL_PROJECT_ID_EXPLORER }} - VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - SENTRY_DSN: ${{ secrets.SENTRY_DSN }} - REACT_APP_PINATA_API_KEY: ${{ secrets.REACT_APP_PINATA_API_KEY }} - REACT_APP_PINATA_SECRET_API_KEY: ${{ secrets.REACT_APP_PINATA_SECRET_API_KEY }} - REACT_APP_BLOCKNATIVE_API_KEY: ${{ secrets.REACT_APP_BLOCKNATIVE_API_KEY }} - REACT_APP_GOOGLE_ANALYTICS_ID: ${{ secrets.REACT_APP_GOOGLE_ANALYTICS_ID }} - REACT_APP_LAUNCH_DARKLY_KEY: ${{ secrets.REACT_APP_LAUNCH_DARKLY_KEY }} - REACT_APP_INFURA_KEY: ${{ secrets.REACT_APP_INFURA_KEY }} - REACT_APP_NETWORK_URL_1: ${{ secrets.REACT_APP_NETWORK_URL_1 }} - REACT_APP_NETWORK_URL_56: ${{ secrets.REACT_APP_NETWORK_URL_56 }} - REACT_APP_NETWORK_URL_100: ${{ secrets.REACT_APP_NETWORK_URL_100 }} - REACT_APP_NETWORK_URL_137: ${{ secrets.REACT_APP_NETWORK_URL_137 }} - REACT_APP_NETWORK_URL_8453: ${{ secrets.REACT_APP_NETWORK_URL_8453 }} - REACT_APP_NETWORK_URL_9745: ${{ secrets.REACT_APP_NETWORK_URL_9745 }} - REACT_APP_NETWORK_URL_42161: ${{ secrets.REACT_APP_NETWORK_URL_42161 }} - REACT_APP_NETWORK_URL_43114: ${{ secrets.REACT_APP_NETWORK_URL_43114 }} - REACT_APP_NETWORK_URL_57073: ${{ secrets.REACT_APP_NETWORK_URL_57073 }} - REACT_APP_NETWORK_URL_59144: ${{ secrets.REACT_APP_NETWORK_URL_59144 }} - REACT_APP_NETWORK_URL_11155111: ${{ secrets.REACT_APP_NETWORK_URL_11155111 }} - REACT_APP_WC_PROJECT_ID: ${{ secrets.REACT_APP_WC_PROJECT_ID }} - REACT_APP_IPFS_READ_URI: ${{ secrets.REACT_APP_IPFS_READ_URI }} - EXPLORER_SENTRY_DSN: ${{ secrets.EXPLORER_SENTRY_DSN }} - REACT_APP_SUBGRAPH_URL_MAINNET: ${{ secrets.REACT_APP_SUBGRAPH_URL_MAINNET }} - REACT_APP_SUBGRAPH_URL_ARBITRUM_ONE: ${{ secrets.REACT_APP_SUBGRAPH_URL_ARBITRUM_ONE }} - REACT_APP_SUBGRAPH_URL_BASE: ${{ secrets.REACT_APP_SUBGRAPH_URL_BASE }} - REACT_APP_SUBGRAPH_URL_GNOSIS_CHAIN: ${{ secrets.REACT_APP_SUBGRAPH_URL_GNOSIS_CHAIN }} - BFF_BASE_URL: ${{ secrets.BFF_BASE_URL }} - CMS_BASE_URL: ${{ secrets.CMS_BASE_URL }} - REACT_APP_NEAR_API_KEY: ${{ secrets.REACT_APP_NEAR_API_KEY }} + uses: ./.github/workflows/vercel.yml + secrets: inherit strategy: matrix: - env_name: [staging] # deploys both in parallel + env_name: [ staging ] # deploys both in parallel with: env_name: ${{ matrix.env_name }} # Pick app according to published tag app: ${{ startsWith(github.ref, 'refs/tags/explorer') && 'EXPLORER' || 'COWSWAP' }} - checkout_ref: ${{ needs.validate-release-tag.outputs.release-commit }} disable_nx_cache: true vercel-prod: @@ -128,7 +62,7 @@ jobs: notify-failure: name: Notify Slack on Failure - needs: [vercel-dev, vercel-pre-prod, vercel-prod] + needs: [ vercel-dev, vercel-pre-prod, vercel-prod ] runs-on: ubuntu-latest if: failure() && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') diff --git a/.github/workflows/vercel.yml b/.github/workflows/vercel.yml index ccc2fafe778..16d99d750b6 100644 --- a/.github/workflows/vercel.yml +++ b/.github/workflows/vercel.yml @@ -11,11 +11,6 @@ on: description: 'Application to deploy. Options are: COWSWAP, EXPLORER' required: true type: string - checkout_ref: - description: 'Exact git ref or commit to check out before building' - required: false - type: string - default: '' disable_nx_cache: description: 'Disable NX cache' required: false @@ -39,7 +34,6 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - ref: ${{ inputs.checkout_ref != '' && inputs.checkout_ref || github.sha }} - name: Install pnpm uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 From 3b16d1f2250315e31a71b39fdb7b42368ac06e6e Mon Sep 17 00:00:00 2001 From: fairlighteth <31534717+fairlighteth@users.noreply.github.com> Date: Tue, 16 Jun 2026 10:23:34 +0100 Subject: [PATCH 5/5] fix(widget): drop hostile asset params from generated snippets - remove invalid sell and buy asset values before serialization - keep generic HTML snippet escaping coverage on allowed string fields - preserve valid trade asset output in the Typescript snippet tests --- .../utils/formatParameters.test.ts | 23 ++++++++++++++++++- .../embedDialog/utils/sanitizeParameters.ts | 18 +++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/apps/widget-configurator/src/app/embedDialog/utils/formatParameters.test.ts b/apps/widget-configurator/src/app/embedDialog/utils/formatParameters.test.ts index d5ddd0a94f0..b0f533d96ca 100644 --- a/apps/widget-configurator/src/app/embedDialog/utils/formatParameters.test.ts +++ b/apps/widget-configurator/src/app/embedDialog/utils/formatParameters.test.ts @@ -14,12 +14,29 @@ const defaultPalette: ColorPalette = { } describe('widget snippet serialization', () => { - it('escapes script-breaking token values in the HTML snippet', () => { + it('drops hostile trade asset values from generated snippets', () => { const params: CowSwapWidgetParams = { appCode: 'Widget App', sell: { asset: '', }, + buy: { + asset: '">', + }, + } + + const snippet = vanillaNoDepsExample(params, defaultPalette) + + expect(snippet).not.toContain('"sell":') + expect(snippet).not.toContain('"buy":') + expect(snippet).not.toContain('alert(1)') + expect(snippet).not.toContain('\\u003c/script\\u003e') + }) + + it('escapes script-breaking string values that are still allowed in HTML snippets', () => { + const params: CowSwapWidgetParams = { + appCode: 'Widget App', + tokenLists: ['https://example.com/'], } const snippet = vanillaNoDepsExample(params, defaultPalette) @@ -45,6 +62,8 @@ describe('widget snippet serialization', () => { appCode: 'Widget App', tradeType: TradeType.ADVANCED, enabledTradeTypes: [TradeType.SWAP, TradeType.YIELD], + sell: { asset: 'USDC', amount: '100000' }, + buy: { asset: 'COW', amount: '0' }, } const validSnippet = tsExample(validParams, defaultPalette) @@ -52,6 +71,8 @@ describe('widget snippet serialization', () => { expect(validSnippet).toContain('TradeType.ADVANCED') expect(validSnippet).toContain('TradeType.SWAP') expect(validSnippet).toContain('TradeType.YIELD') + expect(validSnippet).toContain('"asset": "USDC"') + expect(validSnippet).toContain('"asset": "COW"') const invalidParams = { appCode: 'Widget App', diff --git a/apps/widget-configurator/src/app/embedDialog/utils/sanitizeParameters.ts b/apps/widget-configurator/src/app/embedDialog/utils/sanitizeParameters.ts index cbe672357af..244ac166893 100644 --- a/apps/widget-configurator/src/app/embedDialog/utils/sanitizeParameters.ts +++ b/apps/widget-configurator/src/app/embedDialog/utils/sanitizeParameters.ts @@ -4,6 +4,7 @@ import { CowSwapWidgetParams, TradeType, } from '@cowprotocol/widget-lib' +import { isAddress } from '@cowprotocol/common-utils' import { ColorPalette } from '../../configurator/types' import { SANITIZE_PARAMS } from '../const' @@ -17,6 +18,9 @@ export function sanitizeParameters(params: CowSwapWidgetParams, defaultPalette: theme: sanitizePalette(params, defaultPalette), } + sanitized.sell = sanitizeTradeAsset(sanitized.sell) + sanitized.buy = sanitizeTradeAsset(sanitized.buy) + if (!isTradeType(sanitized.tradeType)) { delete sanitized.tradeType } @@ -56,3 +60,17 @@ function sanitizePalette(params: CowSwapWidgetParams, defaultPalette: ColorPalet function isTradeType(value: unknown): value is TradeType { return Object.values(TradeType).includes(value as TradeType) } + +function sanitizeTradeAsset( + value: CowSwapWidgetParams['sell'] | CowSwapWidgetParams['buy'], +): CowSwapWidgetParams['sell'] | CowSwapWidgetParams['buy'] | undefined { + if (!value || typeof value !== 'object' || !('asset' in value) || typeof value.asset !== 'string') { + return undefined + } + + return isTradeAssetIdentifier(value.asset) ? value : undefined +} + +function isTradeAssetIdentifier(value: string): boolean { + return Boolean(isAddress(value)) || /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(value) +}