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)
+}