Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
lts/jod
22.15.1
36 changes: 33 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ peer-to-peer among its users or into any on-chain liquidity source while
providing MEV protection.

| **Platform** | **Link** |
| --------------------- | ------------------------------------------------------------------------------------------------------------- |
|-----------------------|---------------------------------------------------------------------------------------------------------------|
| 🐮 **CoW Swap** 🐮 | [swap.cow.fi](https://swap.cow.fi/) |
| CoW Swap (IPFS) | Every release is deployed automatically to IPFS ([Releases](https://github.com/cowprotocol/cowswap/releases)) |
| CoW Swap (ENS) | [ens://cowswap.eth](ens://cowswap.eth) or ([cowswap.eth.limo](https://cowswap.eth.limo)) |
| CoW Protocol | [cow.fi](https://cow.fi) |
| Docs | [docs.cow.fi](https://docs.cow.fi) |
| Governance (Snapshot) | [snapshot.org/#/cow.eth](https://snapshot.org/#/cow.eth) |
| Stats | [dune.com/cowprotocol/cowswap](https://dune.com/cowprotocol/cow-swap-home) |
| Stats | [dune.com/cowprotocol/cowswap](https://dune.com/cowprotocol/cow-swap-home) |
| X/Twitter | [@CoWSwap](https://twitter.com/CoWSwap) |
| Discord | [discord.com/invite/cowprotocol](https://discord.com/invite/cowprotocol) |
| Forum | [forum.cow.fi](https://forum.cow.fi) |
Expand Down Expand Up @@ -67,6 +67,18 @@ Build the project. The static files will be generated in the `build` folder.
pnpm run build
```

### Sentry Sourcemaps

`cowswap-frontend` generates production sourcemaps for Sentry.

- Runtime error reporting still uses `REACT_APP_SENTRY_DSN`.
- Build-time sourcemap upload uses `SENTRY_AUTH_TOKEN`.
- `SENTRY_ORG` and `SENTRY_PROJECT` are optional overrides.
Defaults:
`cowprotocol` and `cowswap`

If `SENTRY_AUTH_TOKEN` is not set, the build still succeeds, but sourcemaps are not uploaded to Sentry.

## Unit testing

```bash
Expand Down Expand Up @@ -219,6 +231,24 @@ The API endpoint is configured using the environment variable
REACT_APP_ORDER_BOOK_URLS='{"1":"https://YOUR_HOST","100":"https://YOUR_HOST","5":"https://YOUR_HOST"}
```

## Sentry Configuration

For `apps/cowswap-frontend`:

```ini
REACT_APP_SENTRY_DSN=https://<public-dsn>
SENTRY_AUTH_TOKEN=<sentry-auth-token>
```

Optional overrides for the build-time upload target:

```ini
SENTRY_ORG=cowprotocol
SENTRY_PROJECT=cowswap
```

`SENTRY_AUTH_TOKEN` is the only required secret for source map upload. The org and project values are public identifiers and already default to the values above.

## BFF API Endpoints (Backend for Frontend)

The BFF API is a helper API that provides some additional data to the frontend.
Expand Down Expand Up @@ -263,7 +293,7 @@ All price feeds are enabled by default, but they can be individually disabled by
using an environment variable:

| Name | Environment variable | Type | Description |
| --------- | ------------------------------------ | ---------------------------- | ------------------------------------------------------------------------------------ |
|-----------|--------------------------------------|------------------------------|--------------------------------------------------------------------------------------|
| **1inch** | `REACT_APP_PRICE_FEED_1INCH_ENABLED` | `boolean` (default = `true`) | [Paraswap](https://1inch.exchange) price estimation. Used for all price estimations. |
| **0x** | `REACT_APP_PRICE_FEED_0X_ENABLED` | `boolean` (default = `true`) | [0x](https://0x.org/) price estimation. Used for all price estimation. |

Expand Down
7 changes: 7 additions & 0 deletions apps/cowswap-frontend/.env
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@
# Sentry
#REACT_APP_SENTRY_DSN='https://<url>'
#REACT_APP_SENTRY_TRACES_SAMPLE_RATE="1.0"
#
# Production sourcemaps can be uploaded during the build with:
# SENTRY_AUTH_TOKEN='<token>'
#
# Optional overrides for the upload target. These default to the values below.
# SENTRY_ORG='cowprotocol'
# SENTRY_PROJECT='cowswap'

# IPFS uploading
#REACT_APP_PINATA_API_KEY=
Expand Down
6 changes: 6 additions & 0 deletions apps/cowswap-frontend/public/_headers
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
Content-Security-Policy: default-src 'self'; script-src 'self' https://*.cow.fi https://telegram.org https://*.appzi.io https://www.googletagmanager.com https://s3.amazonaws.com https://www.redditstatic.com https://www.google-analytics.com https://www.clarity.ms https://tag.safary.club https://static.ads-twitter.com https://scripts.clarity.ms https://api.hypelab.com https://googleads.g.doubleclick.net https://tag.adrsbl.io https://cdn.spindl.xyz https://r2.ixncdn.com https://cdn.id5-sync.com 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; img-src 'self' data: blob: https:; font-src * data:; connect-src *; media-src * data: blob:; frame-src *; frame-ancestors *; worker-src 'self'; manifest-src 'self'; base-uri 'self'; form-action 'self'; object-src 'none'; upgrade-insecure-requests;
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot May 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

What are the security risks of frame-ancestors wildcard in Content Security Policy for DeFi applications?

💡 Result:

Using CSP frame-ancestors with a wildcard like '*' is generally high risk for DeFi apps because it allows any website to embed your UI, enabling clickjacking (UI redressing) and other framing-based user deception. 1) Direct clickjacking / UI redressing risk - The frame-ancestors directive controls which parent origins may embed your page in an iframe (and related embedding elements) [1]. If you effectively allow all origins (e.g., via a wildcard), attackers can frame your DeFi app inside their own site and trick users into clicking UI they believe they control. - This “unauthorized framing” is described as enabling clickjacking attacks in CSP wildcard findings [2]. 2) Bypass of the security boundary you intended for “sensitive” screens - DeFi frontends are particularly sensitive because UI actions often lead to signing or confirming transfers/approvals. Guidance on clickjacking defenses emphasizes that protections must be applied to every page with interactive elements (not just a login page), since attackers target the highest-value actions like transfer confirmations and other authenticated operations [3]. A wildcard frame-ancestors undermines that boundary by letting any origin embed those sensitive pages. 3) Increased blast radius for frontend compromise scenarios - In DeFi, trust failure in the frontend delivery path can translate into harmful user interactions (e.g., deceptive transaction prompts, routing/approval abuse) [4][5]. While frame-ancestors wildcard is not the same as script-src being overly permissive, letting any origin embed your app expands the opportunities for attackers to present deceptive UI flows around those compromised or manipulated states. 4) Policy “failure mode” is easy to misunderstand - If you accidentally deploy a too-permissive frame-ancestors source expression, it effectively changes who can embed your site; frame-ancestors does not fall back from default-src [1]. So relying on other CSP directives or default-src for safety while using an over-broad frame-ancestors expression can leave your app embeddable by unintended parties. Practical DeFi guidance (safer patterns) - Prefer denying all embedding unless you have a business need: frame-ancestors 'none' [1]. - If you must allow embedding, use an explicit allowlist of exact trusted origins (and their required subdomains) rather than broad wildcards; the directive supports allowing specific sources, and the allowed set determines embedding authorization [1]. In short: frame-ancestors wildcard (allowing arbitrary origins) removes the main technical protection against hostile framing and increases clickjacking risk for DeFi user interactions, making it a bad fit for hardened DeFi security posture.

Citations:


🏁 Script executed:

cat -n apps/cowswap-frontend/public/_headers | head -20

Repository: cowprotocol/cowswap

Length of output: 1100


Critical: frame-ancestors * allows clickjacking attacks.

The CSP directive frame-ancestors * permits this application to be embedded in iframes on any untrusted website. For a DeFi application handling financial transactions, this creates a serious clickjacking risk where attackers can overlay transparent iframes to trick users into authorizing unintended transactions.

Restrict to trusted domains or block iframe embedding entirely:

  • frame-ancestors 'self' (only allow same-origin embedding)
  • frame-ancestors 'none' (block all iframe embedding)
  • Or explicitly list trusted domains: frame-ancestors 'self' https://trusted-partner.com
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cowswap-frontend/public/_headers` at line 2, The Content-Security-Policy
header currently sets "frame-ancestors *", which permits clickjacking; update
the CSP line in public/_headers to replace frame-ancestors * with a restrictive
value (e.g., frame-ancestors 'self' or frame-ancestors 'none', or an explicit
allowlist like frame-ancestors 'self' https://trusted-partner.com) and ensure
the rest of the policy string remains unchanged; locate the CSP header string
(the long line beginning with "Content-Security-Policy:") and edit only the
frame-ancestors token to the chosen restrictive option.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do embed the page in an iframe when running the widget (see widget.cow.fi for an example).
Wouldn't changing this break the widget behaviour?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

Good point — let me check the widget architecture to better understand the embedding pattern.


🏁 Script executed:

#!/bin/bash
# Check if there's a widget app or widget-specific routes/config
fd -t f -e html -e ts -e tsx -e json --full-path | xargs grep -l "widget" 2>/dev/null | grep -i widget | head -20

# Check if there are any existing frame-ancestors or X-Frame-Options settings
rg -rn "frame-ancestors|X-Frame-Options" --type-add 'config:*.{json,yaml,yml,toml,conf,ini,headers,txt}' --type config -g '!node_modules' 2>/dev/null | head -20

# Check for any widget app
fd -t d "widget" --max-depth 3 | head -10

# List apps directory to understand widget vs cowswap-frontend
ls apps/ 2>/dev/null

Length of output: 909


Let me also check how the widget constructs iframe URLs to understand the embedding pattern better.


🏁 Script executed:

#!/bin/bash
cat libs/widget-lib/src/cowSwapWidget.ts
echo "---"
cat libs/widget-lib/src/urlUtils.ts

Length of output: 18895


@alfetopito, you're absolutely right — changing frame-ancestors globally to 'none' or 'self' would break the widget, since libs/widget-lib is designed to embed cowswap-frontend (e.g. swap.cow.fi) as an iframe on any arbitrary third-party website. That's the core product design.

However, there is a meaningful middle ground. Looking at libs/widget-lib/src/urlUtils.ts, widget routes follow the path pattern /:chainId/widget/:tradeType/:sell/:buy. Cloudflare Pages _headers supports path-scoped rules, so you could scope frame-ancestors * only to widget routes and restrict all other routes:

# Non-widget routes - no iframe embedding needed
/*
  Content-Security-Policy: default-src 'self'; ... frame-ancestors 'self'; ...

# Widget routes - must be embeddable anywhere
/:chainId/widget/*
  Content-Security-Policy: default-src 'self'; ... frame-ancestors *; ...

This way:

  • The main app at /swap, /limit, /about, etc. is protected against clickjacking.
  • Widget routes remain embeddable by any third-party host, preserving the widget use case.

It doesn't eliminate the risk entirely (attackers could still frame a widget URL), but it meaningfully reduces the attack surface by limiting the embeddable surface to only the routes that actually need it.

Would you like me to draft the updated _headers file with this split?


✏️ Learnings added
Learnt from: alfetopito
Repo: cowprotocol/cowswap PR: 7401
File: apps/cowswap-frontend/public/_headers:2-2
Timestamp: 2026-05-12T17:23:47.226Z
Learning: In the cowprotocol/cowswap repository, `cowswap-frontend` is intentionally embedded as an iframe on arbitrary third-party websites via the CowSwap widget (libs/widget-lib). Widget routes follow the URL pattern `/:chainId/widget/:tradeType/:sell/:buy`. The default widget host is `https://swap.cow.fi`. `frame-ancestors *` in the CSP is required for the widget use case; however, it can be scoped to widget routes only using Cloudflare Pages path-based `_headers` rules.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the access is limited to app paths, what would happen to static files? Do we need to manually whitelist any path that's supposed to be served by the app?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alfetopito, great question — there are actually two good news here:

1. Static files don't need manual whitelisting for frame-ancestors

frame-ancestors controls which documents are allowed to embed this document in a nested browsing context like an <iframe>. Browsers only enforce frame-ancestors when rendering a response as a document inside a frame. Static assets like JS bundles, CSS, images, and fonts are fetched as subresources — they're never framed directly — so frame-ancestors on their response headers has no meaningful security effect. You don't need to list /assets/*, /static/*, etc. separately.

2. However, the path-scoping approach I suggested has a catch with Cloudflare Pages

An incoming request which matches multiple rules' URL patterns will inherit all rules' headers, and if a header is applied twice, the values are joined with a comma separator.

This means if you had:

/*
  Content-Security-Policy: ... frame-ancestors 'self'; ...

/:chainId/widget/*
  Content-Security-Policy: ... frame-ancestors *; ...

Widget routes would match both rules, and the browser would receive two Content-Security-Policy headers joined with a comma. Per the CSP spec, when multiple CSP headers are present, the browser enforces all of them simultaneously (intersection), so frame-ancestors 'self' + frame-ancestors * would effectively leave only 'self' — the permissive widget rule would be silently negated.

To work around this on Cloudflare Pages, you'd need to:

  • Use !Content-Security-Policy in the widget rule to remove the base rule, then re-add the full widget-specific CSP. But this means maintaining the full CSP string in two places.
  • Or use a Cloudflare Worker to set headers conditionally based on path (more robust but more complex).

Given that complexity, the pragmatic trade-off is to leave frame-ancestors * globally as-is (current state), since:

  • Widget embedding requires it
  • frame-ancestors on static assets is a no-op for browsers
  • The main framing risk is on interactive HTML pages, which realistically need the same policy as widget routes for this app anyway

Would you like me to explore the Worker-based approach, or are you comfortable keeping the current global frame-ancestors *?

Cross-Origin-Opener-Policy: same-origin-allow-popups
Cross-Origin-Resource-Policy: cross-origin
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
34 changes: 34 additions & 0 deletions apps/cowswap-frontend/vite.config.mts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/// <reference types="vitest" />
import { lingui } from '@lingui/vite-plugin'
import { sentryVitePlugin } from '@sentry/vite-plugin'
import react from '@vitejs/plugin-react-swc'
import stdLibBrowser from 'node-stdlib-browser'
import { bundleStats } from 'rollup-plugin-bundle-stats'
Expand All @@ -15,6 +16,8 @@ import viteTsConfigPaths from 'vite-tsconfig-paths'
import { execSync } from 'child_process'
import * as path from 'path'

import pkg from './package.json'

import { formatChunkFileName } from '../../tools/formatChunkFileName'
import { getReactProcessEnv } from '../../tools/getReactProcessEnv'
import { robotsPlugin } from '../../tools/vite-plugins/robotsPlugin'
Expand All @@ -30,6 +33,12 @@ const nodeDepsToInclude = ['crypto', 'stream']

const analyzeBundle = process.env.ANALYZE_BUNDLE === 'true'
const analyzeBundleTemplate: TemplateType = (process.env.ANALYZE_BUNDLE_TEMPLATE as TemplateType) || 'treemap' // "sunburst" | "treemap" | "network" | "raw-data" | "list";
const defaultSentryOrg = 'cowprotocol'
const defaultSentryProject = 'cowswap'
const sentryAuthToken = process.env.SENTRY_AUTH_TOKEN
const sentryOrg = process.env.SENTRY_ORG || defaultSentryOrg
const sentryProject = process.env.SENTRY_PROJECT || defaultSentryProject
const sentryReleaseName = `CowSwap@v${pkg.version}`

// eslint-disable-next-line max-lines-per-function
export default defineConfig(({ mode, isPreview }) => {
Expand Down Expand Up @@ -87,6 +96,31 @@ export default defineConfig(({ mode, isPreview }) => {
plugins.push(bundleStats() as PluginOption)
}

if (isProduction && sentryAuthToken) {
plugins.push(
...sentryVitePlugin({
org: sentryOrg,
project: sentryProject,
authToken: sentryAuthToken,
telemetry: false,
release: {
name: sentryReleaseName,
inject: false,
create: true,
finalize: true,
},
sourcemaps: {
// Use absolute globs so cleanup works both in Nx builds from the repo root
// and direct Vite builds from the app directory.
filesToDeleteAfterUpload: [
path.resolve(__dirname, '../../build/cowswap/**/*.map'),
path.resolve(__dirname, './dist/**/*.map'),
],
},
}),
)
}

// Disable page indexing for non-prod envs
if (!isProduction) {
plugins.push(
Expand Down
91 changes: 91 additions & 0 deletions docs/CLOUDFLARE_PAGES_MIGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Cloudflare Pages Migration Notes

Scope: `apps/cowswap-frontend` and `apps/explorer` only. `cow-fi` is intentionally excluded.

## Repo-backed equivalents

### `apps/cowswap-frontend`

Vercel source of truth:
- `apps/cowswap-frontend/vercel.ts`

Cloudflare Pages equivalent:
- `apps/cowswap-frontend/public/_headers`

Captured in `_headers`:
- `Content-Security-Policy`
- `Cross-Origin-Opener-Policy`
- `Cross-Origin-Resource-Policy`
- `X-Content-Type-Options`
- `Referrer-Policy`

Notes:
- The Vercel redirect that sends non-asset routes to `/` is not copied into `_redirects`.
- Cloudflare Pages already provides SPA fallback for projects without a top-level `404.html`, which matches the current app output.

### `apps/explorer`

Vercel source of truth:
- `apps/explorer/vercel.json`

Cloudflare Pages equivalent:
- No repo file is required for the current rewrite behavior.

Notes:
- The Vercel rewrite from `/(.*)` to `/index.html` is intentionally not translated to `_redirects`.
- Cloudflare Pages SPA fallback covers this app as long as the build output does not introduce a top-level `404.html`.

## Cloudflare Pages UI or API settings

These settings do not have an equivalent static file in this repo for the current setup and must be configured per Pages project:
- Git repository connection
- Production branch
- Preview branch controls
- Root directory
- Build command
- Build output directory
- Environment variables
- Secrets
- Custom domains
- Branch-to-domain mappings, if used

Recommended project settings:

### `cowswap`
- Root directory: repository root
- Build command: `pnpm run install:ci && pnpm run build:cowswap`
- Build output directory: `build/cowswap`
- Build variable: `SKIP_DEPENDENCY_INSTALL=1`
- Secret: `SENTRY_AUTH_TOKEN` for build-time sourcemap upload
- Optional overrides:
- `SENTRY_ORG=cowprotocol`
- `SENTRY_PROJECT=frontend`
- These are already the repo defaults and only need to be set if you want to override them.

### `explorer`
- Root directory: repository root
- Build command: `pnpm run install:ci && pnpm run build:explorer`
- Build output directory: `build/explorer`
- Build variable: `SKIP_DEPENDENCY_INSTALL=1`

## No exact Cloudflare Pages file alternative

### `cowswap` Vercel regex redirect

Current Vercel behavior:
- `source: /((?!#|.*[\\w\\d\\.-]\\.\\w{2,15}$).+)`
- `destination: /`

Why it is not translated:
- Cloudflare Pages `_redirects` does not support the same regex semantics.
- Using a broad `_redirects` rule here would be a behavior change because Pages redirect rules run before header matching and static asset resolution logic differs from the Vercel config.

Fallback used instead:
- Native Cloudflare Pages SPA serving.

## Validation checklist

- `apps/cowswap-frontend/public/_headers` is copied into `build/cowswap/_headers`.
- `build/cowswap` and `build/explorer` do not contain a top-level `404.html`.
- Deep links for both apps resolve correctly on Pages preview deployments.
- `cowswap` document responses include the migrated security headers.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"start:sdk-tools": "nx run sdk-tools:serve",
"build": "pnpm run build:cowswap",
"build:all": "nx run-many -t build --no-cloud",
"build:cowswap": "cross-env NODE_ENV=production nx build cowswap-frontend --no-cloud",
"build:cowswap": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=8192 nx build cowswap-frontend --no-cloud",
"build:explorer": "nx build explorer --no-cloud",
"build:widget": "nx build widget-configurator --no-cloud",
"build:cowfi": "nx build cow-fi --no-cloud && pnpm run postbuild:cowfi",
Expand Down
Loading