Skip to content

fix: validate cow-fi CMS inputs and revalidation flows#7618

Open
fairlighteth wants to merge 4 commits into
developfrom
deepsec/medium-02-cowfi-cms-boundaries
Open

fix: validate cow-fi CMS inputs and revalidation flows#7618
fairlighteth wants to merge 4 commits into
developfrom
deepsec/medium-02-cowfi-cms-boundaries

Conversation

@fairlighteth

@fairlighteth fairlighteth commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Addresses the cow-fi MEDIUM findings around CMS query construction and public cache/search entry points.

This PR hardens the CMS boundary while keeping the learn pages and article search behavior intact:

  • Replaces the unsafe flatten-and-split query serializer with encoded qs serialization.
  • Validates article and topic slugs before building CMS requests.
  • Validates and bounds article search input server-side.
  • Moves revalidation off query-string secrets and constrains it to POST plus allowlisted learn paths/tags.
  • Adds focused regression coverage for slug normalization, search bounds, revalidate payload validation, and query serialization.

To Test

  1. Run targeted cow-fi checks
  • NX_DAEMON=false ./node_modules/.bin/nx run cow-fi:test --runInBand passes.
  • NX_DAEMON=false ./node_modules/.bin/nx run cow-fi:lint --verbose completes with 0 errors.
  1. Verify learn article and topic routes
  • Open a valid article page and confirm it still loads normally.
  • Open a valid topic page and confirm it still loads normally.
  • Try malformed or injected slugs such as bad slug, %2Fetc, or values containing query syntax; they should fail closed instead of altering the CMS query.
  1. Verify article search validation
  • Normal article search still returns results.
  • Empty search text should return the default bounded search request.
  • Oversized search text or invalid pagination values should be rejected server-side instead of being forwarded upstream.
  • page and pageSize should be clamped to the configured limits.
  1. Verify revalidation hardening
  • GET /api/revalidate returns 405.
  • POST /api/revalidate without an auth header returns 401.
  • POST /api/revalidate with a valid bearer secret and an allowlisted learn path succeeds.
  • Requests with unsupported tags or disallowed paths return 400 and do not revalidate arbitrary routes.

Background

Most of these findings shared one root cause: user-controlled values could reach Strapi query construction or cache invalidation without a strong normalization layer first.

This PR centralizes that normalization in cow-fi utilities so routes, server actions, and revalidation all fail closed from the same validation rules.

Summary by CodeRabbit

  • New Features

    • Added strict CMS slug/path and search input validation across article, topic, search, and revalidation flows.
    • Unified search input normalization to enforce pagination and term limits.
  • Bug Fixes

    • Routes now return proper "Not Found" metadata early for invalid slugs/paths.
    • Revalidation endpoint now authenticates via headers and returns clearer error responses.
    • Query parameter serialization improved for stable CMS queries.
  • Tests

    • Added tests covering slug/path validation, search normalization, revalidation requests, and query-params serialization.

@vercel

vercel Bot commented Jun 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cowfi Ready Ready Preview Jun 6, 2026 6:30pm
explorer-dev Ready Ready Preview Jun 6, 2026 6:30pm
storybook Ready Ready Preview Jun 6, 2026 6:30pm
swap-dev Ready Ready Preview Jun 6, 2026 6:30pm
widget-configurator Ready Ready Preview Jun 6, 2026 6:30pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
cosmos Ignored Ignored Jun 6, 2026 6:30pm
sdk-tools Ignored Ignored Preview Jun 6, 2026 6:30pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 53bd0090-84ff-410f-b1ea-13ead3dd2d71

📥 Commits

Reviewing files that changed from the base of the PR and between 5138ef9 and 0920e11.

📒 Files selected for processing (1)
  • apps/cow-fi/app/(learn)/learn/topic/[topicSlug]/page.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/cow-fi/app/(learn)/learn/topic/[topicSlug]/page.tsx

Walkthrough

This PR adds CMS validation/normalization utilities and applies them: slug validation and early 404s in article/topic routes, normalized search input and typed search action results, unified query-parameter serialization, and header-based, normalized revalidation request handling.

Changes

CMS Validation and Input Normalization

Layer / File(s) Summary
Validation and normalization utilities foundation
apps/cow-fi/util/cmsValidation.ts, apps/cow-fi/util/cmsValidation.test.ts, apps/cow-fi/util/queryParams.ts, apps/cow-fi/util/queryParams.test.ts
Implements slug/path validation, revalidation tag and pagination constants, search input normalization, revalidate-request normalization, and updated query-param serialization. Tests cover slug rules, search normalization/pagination clamping, revalidation allowlist, and query param flattening with preserved delimiters.
Page route slug validation
apps/cow-fi/app/(learn)/learn/[article]/page.tsx, apps/cow-fi/app/(learn)/learn/topic/[topicSlug]/page.tsx
Adds isValidCmsSlug checks in generateMetadata and route handlers; invalid slugs now return Not Found metadata and trigger early notFound() before CMS fetches.
Service and action input normalization
apps/cow-fi/app/actions.ts, apps/cow-fi/services/cms/index.ts
searchArticlesAction uses normalizeSearchArticlesInput and returns a typed success/failure result. CMS service normalizes search input, early-returns empty results for blank searches, validates slugs in getBySlugAux, and uses toQueryParams for fetch wiring.
API revalidation request handling
apps/cow-fi/app/api/revalidate/route.ts
Revalidation endpoint reads secret from Authorization or x-revalidate-secret headers, parses/normalizes POST JSON via normalizeRevalidateRequest, conditionally revalidates a provided path, and returns HTTP 400 with clear messages on validation errors.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client
  participant SearchAction as searchArticlesAction
  participant Normalize as normalizeSearchArticlesInput
  participant Service as searchArticlesService
  participant CMS as CMS Client
  
  Client->>SearchAction: call with search input
  SearchAction->>Normalize: validate/normalize term+pagination
  Normalize-->>SearchAction: {searchTerm, page, pageSize}
  SearchAction->>Service: call with normalized input
  Service->>CMS: GET with toQueryParams(query)
  CMS-->>Service: articles + pagination metadata
  Service-->>SearchAction: results
  SearchAction-->>Client: {success: true, data} or {success: false, error}
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • elena-zh
  • shoom3301
  • alfetopito

Poem

🐰 I hop through slugs and tidy every string,
Trimming search terms and guarding revalidate things.
Brackets flatten queries, headers hide the key,
Clean inputs, safe pages — a rabbit’s dev jubilee! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: validate cow-fi CMS inputs and revalidation flows' accurately summarizes the main security-focused changes: input validation for CMS slugs/search and revalidation endpoint hardening.
Description check ✅ Passed The description provides a comprehensive summary, detailed testing steps covering all major changes, and background context. It follows the template structure with Summary, To Test, and Background sections.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch deepsec/medium-02-cowfi-cms-boundaries

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 6, 2026

Copy link
Copy Markdown

Deploying explorer-dev with  Cloudflare Pages  Cloudflare Pages

Latest commit: 0920e11
Status: ✅  Deploy successful!
Preview URL: https://836b03a9.explorer-dev-dxz.pages.dev
Branch Preview URL: https://deepsec-medium-02-cowfi-cms.explorer-dev-dxz.pages.dev

View logs

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 6, 2026

Copy link
Copy Markdown

Deploying swap-dev with  Cloudflare Pages  Cloudflare Pages

Latest commit: 0920e11
Status: ✅  Deploy successful!
Preview URL: https://6d26327e.swap-dev-5u6.pages.dev
Branch Preview URL: https://deepsec-medium-02-cowfi-cms.swap-dev-5u6.pages.dev

View logs

@fairlighteth fairlighteth left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

AI Review (Codex GPT-5, worked 2m): follow-up addressed

Rechecked

  • Author response: follow-up commit 5138ef9f3bc6d4645d4502b400ddb8acd6699b8f (fix: reject malformed cow-fi CMS validator inputs).
  • Code path: apps/cow-fi/util/cmsValidation.ts.
  • Test coverage: apps/cow-fi/util/cmsValidation.test.ts.

Result: Fixed. The current code validates page and pageSize before the blank-search early return and rejects non-plain-object revalidation payloads, so the prior malformed-input paths no longer pass through normalization.

🤖 Verification notes for AI agents
Verify the prior finding against current code only. Confirm whether the author's claimed fix addresses the specific failure mode, and avoid reopening broader or unrelated issues.

Prior finding:
- Blank searches bypassed pagination validation in normalizeSearchArticlesInput()
- normalizeRevalidateRequest() accepted arrays/non-plain objects because the object guard was too loose

Current verification:
- normalizeSearchArticlesInput() now validates page/pageSize before the blank-search early return
- normalizeRevalidateRequest() now rejects non-plain objects
- cmsValidation regression tests cover both malformed-input cases and pass on the current head

@fairlighteth fairlighteth marked this pull request as ready for review June 6, 2026 14:57

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (2)
apps/cow-fi/app/api/revalidate/route.ts (1)

33-33: ⚡ Quick win

Consider constant-time comparison for secret validation.

Direct string comparison with !== can be vulnerable to timing attacks, where an attacker measures response times to infer secret characters. For authentication secrets, using crypto.timingSafeEqual is best practice.

🔒 Proposed fix using timingSafeEqual
+import { timingSafeEqual } from 'crypto'
+
+function constantTimeCompare(a: string, b: string): boolean {
+  const aBuffer = Buffer.from(a)
+  const bBuffer = Buffer.from(b)
+  if (aBuffer.length !== bBuffer.length) {
+    return false
+  }
+  return timingSafeEqual(aBuffer, bBuffer)
+}
+
 // Check for secret to confirm this is a valid request
-  if (secret !== REVALIDATE_SECRET) {
+  if (!secret || !constantTimeCompare(secret, REVALIDATE_SECRET)) {
     return NextResponse.json({ message: 'Invalid secret' }, { status: 401 })
   }
🤖 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/cow-fi/app/api/revalidate/route.ts` at line 33, Replace the direct
string comparison (secret !== REVALIDATE_SECRET) with a constant-time
comparison: ensure both values are Buffers of the same length, return
unauthorized immediately if lengths differ, then use crypto.timingSafeEqual to
compare the Buffers; update the route handler that reads secret and
REVALIDATE_SECRET to perform this check and reject requests when timingSafeEqual
returns false.
apps/cow-fi/app/(learn)/learn/topic/[topicSlug]/page.tsx (1)

40-46: ⚡ Quick win

Add null category check for consistent metadata handling.

After validation, getCategoryBySlug can still return null if the category doesn't exist in the CMS. Currently, category?.attributes || {} yields an empty object, making name undefined, which results in metadata like "undefined - Knowledge base".

The article page (lines 57-62 in learn/[article]/page.tsx) explicitly checks if (!article || !article.attributes) and returns proper "Article Not Found" metadata. Consider applying the same pattern here for consistency.

♻️ Suggested fix
  const category = await getCategoryBySlug(topicSlug)
+ 
+ if (!category || !category.attributes) {
+   return getPageMetadata({
+     absoluteTitle: 'Topic Not Found - Knowledge base',
+     description: 'The requested topic could not be found.',
+   })
+ }
+ 
  const { name, description = '' } = category.attributes

  return getPageMetadata({
    absoluteTitle: `${name} - Knowledge base`,
    description,
  })
🤖 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/cow-fi/app/`(learn)/learn/topic/[topicSlug]/page.tsx around lines 40 -
46, The metadata generation can produce "undefined - Knowledge base" because
category may be null; update the block that calls getCategoryBySlug(topicSlug)
to explicitly check if (!category || !category.attributes) and return a
not-found metadata via getPageMetadata (e.g., title "Category Not Found" and a
suitable description) before destructuring attributes; keep the existing return
path that uses const { name, description = '' } = category.attributes when
category is present.
🤖 Prompt for all review comments with 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.

Nitpick comments:
In `@apps/cow-fi/app/`(learn)/learn/topic/[topicSlug]/page.tsx:
- Around line 40-46: The metadata generation can produce "undefined - Knowledge
base" because category may be null; update the block that calls
getCategoryBySlug(topicSlug) to explicitly check if (!category ||
!category.attributes) and return a not-found metadata via getPageMetadata (e.g.,
title "Category Not Found" and a suitable description) before destructuring
attributes; keep the existing return path that uses const { name, description =
'' } = category.attributes when category is present.

In `@apps/cow-fi/app/api/revalidate/route.ts`:
- Line 33: Replace the direct string comparison (secret !== REVALIDATE_SECRET)
with a constant-time comparison: ensure both values are Buffers of the same
length, return unauthorized immediately if lengths differ, then use
crypto.timingSafeEqual to compare the Buffers; update the route handler that
reads secret and REVALIDATE_SECRET to perform this check and reject requests
when timingSafeEqual returns false.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c4857883-066b-40d5-9f7d-f4eff21c856a

📥 Commits

Reviewing files that changed from the base of the PR and between 9c7abf2 and 5138ef9.

📒 Files selected for processing (9)
  • apps/cow-fi/app/(learn)/learn/[article]/page.tsx
  • apps/cow-fi/app/(learn)/learn/topic/[topicSlug]/page.tsx
  • apps/cow-fi/app/actions.ts
  • apps/cow-fi/app/api/revalidate/route.ts
  • apps/cow-fi/services/cms/index.ts
  • apps/cow-fi/util/cmsValidation.test.ts
  • apps/cow-fi/util/cmsValidation.ts
  • apps/cow-fi/util/queryParams.test.ts
  • apps/cow-fi/util/queryParams.ts

@fairlighteth

Copy link
Copy Markdown
Contributor Author

@coderabbitai following up on #7618 (review)

I fixed the topic metadata issue in 0920e11cd7c2a5ae42861c0ac41293158b012af4 (fix: return not-found metadata for missing cow-fi topics). generateMetadata() in apps/cow-fi/app/(learn)/learn/topic/[topicSlug]/page.tsx now returns the same not-found metadata when getCategoryBySlug() returns null or missing attributes, so it no longer produces "undefined - Knowledge base".

I did not apply the timingSafeEqual suggestion in app/api/revalidate/route.ts. I consider that a non-blocking hardening improvement rather than a practical bug for this endpoint, so I left it out of this PR's scope.

@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@fairlighteth Thanks for the follow-up!

  • Topic metadata fix: Confirmed — generateMetadata() now correctly falls back to not-found metadata when getCategoryBySlug() returns null or attributes are missing. That resolves the "undefined - Knowledge base" issue cleanly.

  • timingSafeEqual deferral: Understood and acknowledged. The current === comparison for the bearer secret is a low-risk timing-channel concern at this scale, so deferring it from this PR's scope is reasonable. Would you like me to open a follow-up GitHub issue to track the hardening improvement for app/api/revalidate/route.ts so it doesn't get lost?


✏️ Learnings added
Learnt from: fairlighteth
URL: https://github.com/cowprotocol/cowswap/pull/7618

Timestamp: 2026-06-06T18:28:31.534Z
Learning: In `apps/cow-fi/app/api/revalidate/route.ts`, the team intentionally deferred the `timingSafeEqual`-based constant-time comparison for the bearer secret (replacing the current `===` string comparison) from PR `#7618`. It was considered a non-blocking hardening improvement rather than a practical bug. A follow-up issue should track this hardening task.

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.


🧠 Learnings used
Learnt from: kernelwhisperer
Repo: cowprotocol/cowswap PR: 7099
File: apps/cowswap-frontend/src/modules/affiliate/misc/traders.sql:73-73
Timestamp: 2026-03-05T08:09:59.375Z
Learning: In `apps/cowswap-frontend/src/modules/affiliate/misc/traders.sql`, the `first_trade` CTE still includes integrator-origin swaps (rows where `is_excluded_integrators_source = true`). This means traders whose earliest swap came via an integrator (e.g., Safe App) can still be incorrectly marked as ineligible at the eligibility gate (`first_trade.first_trade_time = first_ref_trade.first_ref_trade_time`). The fix — adding `WHERE NOT is_excluded_integrators_source` to the `first_trade` CTE — was acknowledged as a valid concern in PR `#7099` and intentionally deferred to a later PR.

Learnt from: kernelwhisperer
Repo: cowprotocol/cowswap PR: 7099
File: apps/cowswap-frontend/src/modules/affiliate/api/checkIfTraderHasPastTrades.ts:18-19
Timestamp: 2026-03-05T08:08:18.725Z
Learning: In `apps/cowswap-frontend/src/modules/affiliate/api/checkIfTraderHasPastTrades.ts`, the intended future improvement for eligibility checking is: fetch the latest ~10 orders per chain/env (using `getOrders` rather than `getTrades`) and filter out integrator-origin orders as well as canceled/expired ones before setting `hasPastTrades = true`. The current implementation (as of PR `#7099`) does not yet apply these filters and intentionally defers them to a later PR.

Learnt from: kernelwhisperer
Repo: cowprotocol/cowswap PR: 6793
File: apps/cowswap-frontend/src/modules/affiliate/api/checkIfTraderHasPastTrades.ts:9-21
Timestamp: 2026-02-24T09:50:47.608Z
Learning: Repo cowprotocol/cowswap: For Orderbook (cow-sdk) calls like orderBookApi.getTrades used in affiliate eligibility checks (e.g., apps/cowswap-frontend/src/modules/affiliate/api/checkIfTraderHasPastTrades.ts), the team prefers relying on the cow-sdk’s built-in rate limiting and backoff (configured in apps/cowswap-frontend/src/cowSdk.ts via DEFAULT_BACKOFF_OPTIONS) and does not want additional frontend-level request timeouts layered on top.

Learnt from: Danziger
Repo: cowprotocol/cowswap PR: 7153
File: apps/cowswap-frontend/src/modules/advancedOrders/pure/Settings/AdvancedOrdersSettings.tsx:25-25
Timestamp: 2026-03-16T16:33:33.440Z
Learning: In `apps/cowswap-frontend/src/modules/advancedOrders/pure/Settings/AdvancedOrdersSettings.tsx`, the comment `// TODO: we should use limit orders settings in Advanced Orders!` (line 25) is a pre-existing TODO that predates PR `#7153`. It is intentionally deferred and should not be flagged as a blocker in reviews of that PR or subsequent PRs unless there is a dedicated effort to address it.

Learnt from: fairlighteth
Repo: cowprotocol/cowswap PR: 5768
File: apps/cow-fi/components/LearnPageComponent.tsx:184-185
Timestamp: 2025-05-28T16:50:12.273Z
Learning: In apps/cow-fi/components/LearnPageComponent.tsx, the user prefers to keep the inconsistent link behavior where featured articles always open in new tabs (target="_blank") while media coverage links conditionally open in new tabs based on the linkExternal flag. This inconsistency should not be flagged as an issue in future reviews.

Learnt from: CR
Repo: cowprotocol/cowswap PR: 0
File: apps/cowswap-frontend/AGENTS.md:0-0
Timestamp: 2026-05-22T12:57:32.776Z
Learning: Applies to apps/cowswap-frontend/**/*.{ts,tsx} : Use explicit return types where they improve safety/readability. Keep local trivial internals inferred; keep exported APIs aligned with root `AGENTS.md` requirements

Learnt from: kernelwhisperer
Repo: cowprotocol/cowswap PR: 7111
File: apps/cowswap-frontend/src/modules/affiliate/api/checkIfTraderHasPastTrades.ts:49-53
Timestamp: 2026-03-06T11:37:06.263Z
Learning: In `apps/cowswap-frontend/src/modules/affiliate/api/checkIfTraderHasPastTrades.ts`, picking `refCode` via `flat().find(...)` in chain/env iteration order (rather than earliest chronological order) is intentional and acceptable. Affiliate ref codes are only issued through the CoW Swap UI, so all legitimate orders across chains carry the same code; divergence only occurs if a user bypasses the UI, which is an unsupported path the team explicitly does not handle.

@fairlighteth

Copy link
Copy Markdown
Contributor Author

@coderabbitai yes, please create the follow-up issue for the timingSafeEqual hardening in apps/cow-fi/app/api/revalidate/route.ts.

@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
gh issue create \
  --repo cowprotocol/cowswap \
  --title "hardening: use timingSafeEqual for bearer secret comparison in cow-fi revalidate route" \
  --assignee fairlighteth \
  --body "## Summary

The revalidation API route at \`apps/cow-fi/app/api/revalidate/route.ts\` currently compares the bearer secret using a plain \`===\` string comparison. This is susceptible to timing-based side-channel attacks, where an attacker could infer the secret length or partial matches through response timing differences.

## Suggested improvement

Replace the \`===\` comparison with Node.js \`crypto.timingSafeEqual\` to perform a constant-time comparison, eliminating the timing side-channel.

Example:
\`\`\`ts
import { timingSafeEqual } from 'crypto'

function isValidSecret(provided: string, expected: string): boolean {
  const a = Buffer.from(provided)
  const b = Buffer.from(expected)
  if (a.length !== b.length) return false
  return timingSafeEqual(a, b)
}
\`\`\`

## Context

This was identified during review of PR `#7618` (https://github.com/cowprotocol/cowswap/pull/7618) and intentionally deferred as a non-blocking hardening improvement rather than a practical bug for this endpoint. See the discussion at https://github.com/cowprotocol/cowswap/pull/7618#issuecomment-0.

Requested by `@fairlighteth`."

Length of output: 1417



@fairlighteth: The follow-up issue has been created and assigned to you:

#7624 — hardening: use timingSafeEqual for bearer secret comparison in cow-fi revalidate route

It documents the timing side-channel concern in apps/cow-fi/app/api/revalidate/route.ts, includes a code snippet for the timingSafeEqual fix, and back-links to PR #7618.


🧠 Learnings used
Learnt from: fairlighteth
Repo: cowprotocol/cowswap PR: 0
File: :0-0
Timestamp: 2026-06-06T18:28:31.534Z
Learning: In `apps/cow-fi/app/api/revalidate/route.ts`, the team intentionally deferred the `timingSafeEqual`-based constant-time comparison for the bearer secret (replacing the current `===` string comparison) from PR `#7618`. It was considered a non-blocking hardening improvement rather than a practical bug. A follow-up issue should track this hardening task.

Learnt from: kernelwhisperer
Repo: cowprotocol/cowswap PR: 7099
File: apps/cowswap-frontend/src/modules/affiliate/misc/traders.sql:73-73
Timestamp: 2026-03-05T08:09:59.375Z
Learning: In `apps/cowswap-frontend/src/modules/affiliate/misc/traders.sql`, the `first_trade` CTE still includes integrator-origin swaps (rows where `is_excluded_integrators_source = true`). This means traders whose earliest swap came via an integrator (e.g., Safe App) can still be incorrectly marked as ineligible at the eligibility gate (`first_trade.first_trade_time = first_ref_trade.first_ref_trade_time`). The fix — adding `WHERE NOT is_excluded_integrators_source` to the `first_trade` CTE — was acknowledged as a valid concern in PR `#7099` and intentionally deferred to a later PR.

Learnt from: kernelwhisperer
Repo: cowprotocol/cowswap PR: 7099
File: apps/cowswap-frontend/src/modules/affiliate/api/checkIfTraderHasPastTrades.ts:18-19
Timestamp: 2026-03-05T08:08:18.725Z
Learning: In `apps/cowswap-frontend/src/modules/affiliate/api/checkIfTraderHasPastTrades.ts`, the intended future improvement for eligibility checking is: fetch the latest ~10 orders per chain/env (using `getOrders` rather than `getTrades`) and filter out integrator-origin orders as well as canceled/expired ones before setting `hasPastTrades = true`. The current implementation (as of PR `#7099`) does not yet apply these filters and intentionally defers them to a later PR.

Learnt from: kernelwhisperer
Repo: cowprotocol/cowswap PR: 7111
File: apps/cowswap-frontend/src/modules/affiliate/api/checkIfTraderHasPastTrades.ts:49-53
Timestamp: 2026-03-06T11:37:06.263Z
Learning: In `apps/cowswap-frontend/src/modules/affiliate/api/checkIfTraderHasPastTrades.ts`, picking `refCode` via `flat().find(...)` in chain/env iteration order (rather than earliest chronological order) is intentional and acceptable. Affiliate ref codes are only issued through the CoW Swap UI, so all legitimate orders across chains carry the same code; divergence only occurs if a user bypasses the UI, which is an unsupported path the team explicitly does not handle.

Learnt from: Danziger
Repo: cowprotocol/cowswap PR: 7153
File: apps/cowswap-frontend/src/modules/advancedOrders/pure/Settings/AdvancedOrdersSettings.tsx:25-25
Timestamp: 2026-03-16T16:33:33.440Z
Learning: In `apps/cowswap-frontend/src/modules/advancedOrders/pure/Settings/AdvancedOrdersSettings.tsx`, the comment `// TODO: we should use limit orders settings in Advanced Orders!` (line 25) is a pre-existing TODO that predates PR `#7153`. It is intentionally deferred and should not be flagged as a blocker in reviews of that PR or subsequent PRs unless there is a dedicated effort to address it.

Learnt from: kernelwhisperer
Repo: cowprotocol/cowswap PR: 6793
File: apps/cowswap-frontend/src/modules/affiliate/api/checkIfTraderHasPastTrades.ts:9-21
Timestamp: 2026-02-24T09:50:47.608Z
Learning: Repo cowprotocol/cowswap: For Orderbook (cow-sdk) calls like orderBookApi.getTrades used in affiliate eligibility checks (e.g., apps/cowswap-frontend/src/modules/affiliate/api/checkIfTraderHasPastTrades.ts), the team prefers relying on the cow-sdk’s built-in rate limiting and backoff (configured in apps/cowswap-frontend/src/cowSdk.ts via DEFAULT_BACKOFF_OPTIONS) and does not want additional frontend-level request timeouts layered on top.

Learnt from: fairlighteth
Repo: cowprotocol/cowswap PR: 5768
File: apps/cow-fi/components/LearnPageComponent.tsx:184-185
Timestamp: 2025-05-28T16:50:12.273Z
Learning: In apps/cow-fi/components/LearnPageComponent.tsx, the user prefers to keep the inconsistent link behavior where featured articles always open in new tabs (target="_blank") while media coverage links conditionally open in new tabs based on the linkExternal flag. This inconsistency should not be flagged as an issue in future reviews.

Learnt from: Danziger
Repo: cowprotocol/cowswap PR: 6997
File: apps/cowswap-frontend/src/common/state/fetchTokens.utils.ts:37-49
Timestamp: 2026-03-20T18:17:17.482Z
Learning: In `apps/cowswap-frontend/src/common/state/fetchTokens.utils.ts`, the use of `Promise.all` (instead of `Promise.allSettled`) for batching `fetchTokenFromBlockchain` calls in `fetchTokens` is intentional. The app cannot function correctly when any tokens are missing, so failing the entire batch on a single rejection is the desired fail-fast behavior. Do not flag this as a missing error-handling issue.

Learnt from: CR
Repo: cowprotocol/cowswap PR: 0
File: apps/cowswap-frontend/AGENTS.md:0-0
Timestamp: 2026-05-22T12:57:32.776Z
Learning: Applies to apps/cowswap-frontend/**/*.{ts,tsx} : Use explicit return types where they improve safety/readability. Keep local trivial internals inferred; keep exported APIs aligned with root `AGENTS.md` requirements

Learnt from: kernelwhisperer
Repo: cowprotocol/cowswap PR: 7140
File: apps/cowswap-frontend/src/modules/affiliate/pure/TraderIneligible.tsx:15-18
Timestamp: 2026-03-13T12:37:24.570Z
Learning: In `apps/cowswap-frontend/src/modules/affiliate/pure/TraderIneligible.tsx`, the hardcoded `SupportedChainId.MAINNET` in `getExplorerAddressLink` is intentional and correct. It is used as a default chain for the explorer link because the CoW Explorer has no "all-chain" view. When a user lands on the Ethereum explorer page but has orders on another chain (e.g., Arbitrum), the CoW Explorer will display a message such as "No orders found on Ethereum, however found orders on: Arbitrum, Base, ..." guiding them to the right chain. This is not related to payout chain logic (AFFILIATE_PAYOUTS_CHAIN_ID).

@fairlighteth

Copy link
Copy Markdown
Contributor Author
PR - Automated QA: cow-fi CMS boundary hardening verified in browser

Outcome

  • Passed: valid learn article and topic pages still load normally.
  • Passed: malformed article/topic slugs fail closed to not-found pages instead of loading altered content.
  • Passed: learn search still returns expected results, and whitespace-only search falls back cleanly.
  • Passed: public revalidation route rejects unsupported method and missing auth as expected.
  • Follow-up note: authenticated revalidation success and allowlist-400 cases were not testable from the public preview without the secret.

Source under test

  • Browser-based verification ran against the deployed cow-fi preview: https://cowfi-git-deepsec-medium-02-cowfi-cms-boundaries-cowswap.vercel.app
  • Preview commit matched the current PR head: 0920e11cd7c2a5ae42861c0ac41293158b012af4
  • I did not use the local dev server for browser QA because this worktree currently fails locally on missing styled-components/macro, so the preview was the reliable runtime target.
  • Results were visually inspected from captured screenshots.

Environment:

  • Browsers: Chromium 140.0.7339.16 and Firefox 141.0
  • OS: Linux 6.18.33-2-cachyos-lts

Wallet state reached:

  • disconnected: yes
  • provider-injected: no
  • wallet-connected-ui: no
  • signing-capable: no

If re-running locally:

  • Frontend: use the PR preview above, or a local cow-fi dev server once the local styled-components/macro environment issue is resolved
  • Routes: /learn, /learn/aave-trade-breakdown, /learn/topic/mev, /learn/%2Fetc, /learn/topic/Bad-Slug, /api/revalidate
  • Local state required: none
  • Fixtures/mocks: none

Expected results:

  1. Open /learn, search aave, then clear to whitespace.
    Expected: Aave results appear for the search term, and the default featured/article sections remain visible for whitespace-only input.
  2. Open /learn/aave-trade-breakdown and /learn/topic/mev.
    Expected: both pages render normal article/topic content with the expected titles.
  3. Open /learn/%2Fetc and /learn/topic/Bad-Slug, then call the revalidate endpoint without auth.
    Expected: both malformed routes show the 404 page with not-found metadata; GET /api/revalidate returns 405, and unauthenticated POST /api/revalidate returns 401.

Browser coverage:

  • Chromium 140.0.7339.16: /learn load, search success, whitespace-only search fallback, valid article/topic routes, malformed article/topic slugs, GET /api/revalidate 405, unauthenticated POST /api/revalidate 401
  • Firefox 141.0: /learn load, valid article/topic routes, malformed article/topic slugs, GET /api/revalidate 405, unauthenticated POST /api/revalidate 401, plus isolated rerun confirming the aave search path works there too

Why these adjacent flows were included:

  • Whitespace-only search was included because this PR hardens search normalization and previously had a blank-search validation edge case.
  • Malformed slug routes were included because the touched code validates article/topic slugs before CMS fetches.
  • Public GET and unauthenticated POST revalidate checks were included because the PR moves revalidation onto POST plus header auth and should fail closed at the boundary.
Note: Firefox automation flake

Impact:

  • This did not undercut the PR claim, but it is worth recording precisely.

Observed behavior:

  • In the first broad Firefox script pass, getByLabel('Search articles').fill(...) timed out while Playwright still considered navigation unfinished.
  • Re-running that same interaction in isolation on /learn succeeded and produced the expected Aave search results.

Interpretation:

  • The route-level Firefox coverage passed, and the search field worked on isolated rerun, so this looks like automation timing flake rather than a user-visible regression from the PR.

Commands run:

  • gh pr view https://github.com/cowprotocol/cowswap/pull/7618 --json title,body,files,reviews,comments
  • PLAYWRIGHT_BROWSERS_PATH=$HOME/.cache/ms-playwright-local /home/mb/.local/playwright-tools/node_modules/.bin/playwright --version
  • PLAYWRIGHT_BROWSERS_PATH=$HOME/.cache/ms-playwright-local node <Playwright script against the preview covering learn routes/search/revalidate>
  • PLAYWRIGHT_BROWSERS_PATH=$HOME/.cache/ms-playwright-local node <isolated Firefox rerun for the /learn search input>

Still manual if desired:

  • Human visual/readability review of the learn search dropdown and topic/article layouts on mobile breakpoints.
  • Authenticated revalidation success and disallowed-path/tag 400 responses with a real secret in a controlled environment.

Artifacts:

  • Results were captured and visually inspected from local screenshots.
  • Screenshots were not embedded in this comment because I did not publish them to a stable public host.

Residual gaps:

  • I did not verify authenticated revalidation success or disallowed-path/tag 400 cases because the preview secret is not available.
  • Browser QA cannot directly exercise malformed page/pageSize payloads through the public learn UI; those remain covered by the focused unit tests on this branch.

Comment on lines +9 to +17
function getSecretFromHeaders(request: NextRequest): string | null {
const authorization = request.headers.get('authorization')

if (authorization?.startsWith('Bearer ')) {
return authorization.slice('Bearer '.length)
}

return request.headers.get('x-revalidate-secret')
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I recall where this is coming from (invalidating cache manually and so forth). But isn't there a middleware or similar thing built-in in Nextjs for handling authentication?
I don't want to over complicate this, though. Only if it makes sense and it's a small change.

Comment on lines +12 to +14
function isRecord(value: unknown): value is Record<string, unknown> {
return Object.prototype.toString.call(value) === '[object Object]'
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Something similar already exists

export function isRecord(value: unknown): value is JsonRecord {
return typeof value === 'object' && value !== null
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants