Skip to content

fix: audit-2026-05-13 frontend hardening (CSRF, slippage caps, advisories)#80

Merged
github-actions[bot] merged 1 commit into
mainfrom
fix/audit-2026-05-13-frontend-hardening
May 14, 2026
Merged

fix: audit-2026-05-13 frontend hardening (CSRF, slippage caps, advisories)#80
github-actions[bot] merged 1 commit into
mainfrom
fix/audit-2026-05-13-frontend-hardening

Conversation

@satyakwok
Copy link
Copy Markdown
Member

@satyakwok satyakwok commented May 14, 2026

Summary

Audit-driven hardening pass across the SentrisCloud frontend monorepo. Closes 2 HIGH + 7 MEDIUM + 3 LOW findings from the 2026-05-13 internal review. All 8 apps build green; pnpm audit went from 29 vulns (11 HIGH) to 1 MODERATE.

HIGH

  • HIGH 1 — coinblast /api/pin CSRF + DoS (apps/coinblast/src/app/api/pin/route.ts:45-185): added checkOrigin allowlist + 10 pins/hour per-IP limit. Defends operator's Pinata quota / billing against drive-by upload spam.
  • HIGH 2 — coinblast /api/cb proxy CSRF (apps/coinblast/src/app/api/cb/[...path]/route.ts:14-81): origin gate on POST only; GET stays open. Defence-in-depth on top of indexer's owner-signed-metadata gate.

MEDIUM

  • MEDIUM 1 — SVG MIME (apps/coinblast/src/app/api/pin/route.ts:21-27): dropped image/svg+xml. XSS risk if any future component renders via <object> / <iframe> / srcdoc.
  • MEDIUM 2 — IPFS gateway hardcoded (apps/coinblast/src/app/token/[address]/layout.tsx:45-48): route OG-image rewrite through ipfsToGateway() so NEXT_PUBLIC_IPFS_GATEWAY override actually applies.
  • MEDIUM 3 — next.js HIGH advisories (apps/solux/package.json, apps/chain-landing/package.json): bumped next 16.2.3 → 16.2.6 (closes 11 HIGH middleware/proxy advisories on next 16.x).
  • MEDIUM 4 — axios transitive CVEs (root package.json): added pnpm.overrides.axios ^1.15.2. Closes 10 axios advisories incl. CVE-2025-62718 (NO_PROXY SSRF) + GHSA-q8qp-cvcw-x6jj (prototype-pollution gadgets).
  • MEDIUM 5 — faucet XFF spoof (apps/faucet/src/app/api/faucet/route.ts:90-101): in production, require CF-Connecting-IP; reject 503 if missing instead of falling through to spoofable XFF.
  • MEDIUM 6 — coinblast slippage cap (apps/coinblast/src/components/token/BuySellWidget.tsx:64-69): cap user input at 10% (was 15%); add expert-mode confirmation modal for any swap > 5%.
  • MEDIUM 7 — dex slippage cap (apps/dex/src/app/SwapWidget.tsx:60-61): cap at 5% with explicit warning ≥3%; add custom slippage input to SlippageControl.

LOW

  • LOW 1 — solux clipboard self-clear (apps/solux/src/components/Settings.tsx:74): clear on component unmount via useEffect cleanup; toast adds explicit "clear manually" guidance.
  • LOW 2 — airdrop proofs cache busting (apps/airdrop/src/components/ClaimWidget.tsx:70): append ?v=NEXT_PUBLIC_BUILD_ID query so cache busts on schema rotation.
  • LOW 3 — coinblast deploy script chain naming (apps/coinblast/cb-deploy-7-tokens.mjs:42): nativeCurrency.name 'SRX''Sentrix' per chain naming canonical.
  • LOW 4 — gitignores (root .gitignore, apps/solux/.gitignore): added .env.production to root; dropped !.env.production allow-list from solux (was the opposite of intent).

Skipped

  • LOW git-history scrub of apps/faucet/.env.production IP — history rewrite is high-risk, operator-driven decision.

Test plan

  • pnpm install — clean, axios pinned to 1.15.2 across transitive paths
  • pnpm -r typecheck — all 9 packages green
  • pnpm build per-app: airdrop, chain-landing, coinblast, dex, faucet, landing, scan, solux all pass standalone
  • pnpm audit — 29 (11 HIGH) → 1 MODERATE (postcss inside next 15.5.18, awaiting upstream)
  • Smoke: /api/pin from coinblast.sentriscloud.com — verify origin gate doesn't reject same-origin
  • Smoke: /api/pin from foreign origin — verify 403
  • Smoke: 11th pin from same IP within an hour — verify 429 + Retry-After
  • Smoke: faucet from production without CF — verify 503 (rather than falling through)
  • Smoke: coinblast trade with 7% slippage — verify expert-mode modal blocks until ack
  • Smoke: dex slippage input — verify hard-clamp at 5%

Summary by CodeRabbit

  • New Features

    • Added slippage limits and warnings to trading widgets for better transaction control.
    • Implemented clipboard auto-clearing for improved secret management security.
  • Security & Performance

    • Enhanced origin validation for state-changing requests to prevent unauthorized actions.
    • Added rate limiting for file uploads.
    • Restricted upload file types to mitigate security risks.
    • Improved client IP validation in production environments.
    • Implemented cache-busting for dynamic proof data to ensure fresh content.
  • Dependencies

    • Updated Next.js and other dependencies to latest patch versions.

Review Change Stack

CSRF + rate-limit on coinblast write paths
- /api/pin: add origin allowlist + 10 pins/hour per-IP limit; same
  pattern as faucet's /api/faucet. Defends Pinata quota / billing.
- /api/cb POST: same origin gate. GET stays open (read-only).
- Drop image/svg+xml from pin MIME allowlist — XSS risk if any future
  component renders via <object>/<iframe>/srcdoc.
- Route OG-image rewrite through ipfsToGateway() so
  NEXT_PUBLIC_IPFS_GATEWAY override actually applies.

Slippage policy
- coinblast: cap user input at 10% (was 15%); add expert-mode
  confirmation modal for any swap > 5%.
- dex: cap at 5% with explicit warning ≥3%; add custom slippage input
  to SlippageControl.

Advisory churn
- Bump next 16.2.3 → 16.2.6 on solux + chain-landing (closes 11 HIGH
  middleware/proxy advisories on next 16).
- Add pnpm.overrides axios ^1.15.2 — pulls 1.15.2+ for the wagmi →
  @base-org/account → @coinbase/cdp-sdk transitive path. Closes 10
  axios advisories incl. CVE-2025-62718 (NO_PROXY SSRF) +
  GHSA-q8qp-cvcw-x6jj (prototype-pollution gadgets).
- pnpm audit went from 29 vulns (11 high) to 1 moderate (postcss
  inside next 15.5.18, awaiting upstream).

Defence-in-depth
- faucet getClientIP: in production require CF-Connecting-IP; reject
  503 if missing instead of silently falling through to spoofable XFF.
- solux Settings: clear clipboard on component unmount (the existing
  60s setTimeout doesn't fire if tab is backgrounded), and toast
  copy of secrets with explicit "clear manually" guidance.
- airdrop /proofs.json: append ?v=BUILD_ID query so cache busts on
  schema rotation.

Misc
- coinblast cb-deploy-7-tokens.mjs: fix nativeCurrency.name 'SRX' →
  'Sentrix' per chain naming canonical.
- Root .gitignore: add .env.production / .env.production.local.
- apps/solux/.gitignore: drop the !.env.production allow-list (was
  the opposite of the intended deny). Mirror faucet's env stanza.
@github-actions github-actions Bot enabled auto-merge (squash) May 14, 2026 02:20
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 14, 2026

📝 Walkthrough

Walkthrough

This PR is a substantial multi-area hardening and feature release. It adds security infrastructure (origin allowlisting and per-IP rate limiting) to Coinblast API routes, introduces a complete token deployment automation script that pins icons to IPFS and registers owner-signed metadata, tightens slippage risk controls across trading widgets with configurable thresholds and confirmation modals, refactors metadata resolution to use shared IPFS helpers, implements deterministic clipboard clearing in account settings, hardens IP validation in the production faucet, and updates environment ignores and Next.js/axios versions. The changes span infrastructure security, deployment automation, user-facing risk controls, and dependency management.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

dependencies, javascript

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% 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 accurately summarizes the main objective: a frontend security hardening pass addressing audit findings (CSRF, slippage caps, advisories), which is the core of this changeset.
Description check ✅ Passed The PR description comprehensively covers all required sections from the template: Summary (3 sentences explaining what/why), detailed breakdown of all audit findings (HIGH/MEDIUM/LOW), and Test plan with thorough checklist including both completed and pending smoke tests.
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 fix/audit-2026-05-13-frontend-hardening

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
apps/coinblast/src/lib/rateLimit.ts (1)

59-70: ⚡ Quick win

Consider requiring CF-Connecting-IP in production for consistency.

Unlike the faucet route's getClientIP (which hard-requires CF-Connecting-IP in production and returns null otherwise), this implementation always falls back to potentially spoofable headers (X-Real-IP, X-Forwarded-For).

While the checkOrigin gate provides defense-in-depth, aligning this with the faucet's approach would strengthen the rate-limit enforcement: an attacker bypassing the edge proxy (e.g., by hitting the origin directly) could also spoof X-Forwarded-For to bypass per-IP limits.

🔒 Proposed change to require CF-Connecting-IP in production
 export function getClientIP(req: Request): string {
   const cf = req.headers.get("cf-connecting-ip");
   if (cf) return cf.trim();
+  if (process.env.NODE_ENV === "production") {
+    // Missing CF-Connecting-IP in production = potential bypass.
+    // Treat as localhost (single bucket for all unverified traffic).
+    return "127.0.0.1";
+  }
   const realIP = req.headers.get("x-real-ip");
   if (realIP) return realIP.trim();

Note: Returning "127.0.0.1" lumps all unverified traffic into one rate-limit bucket rather than allowing per-spoofed-IP bypass.

🤖 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/coinblast/src/lib/rateLimit.ts` around lines 59 - 70, getClientIP
currently falls back to X-Real-IP/X-Forwarded-For, allowing spoofing; change it
so that in production it requires CF-Connecting-IP only (do not fall back) and
returns null (or the same sentinel used by the faucet route) when
CF-Connecting-IP is missing, while in non-production keep the existing fallback
behavior; update the getClientIP function and align its return sentinel with the
faucet route's getClientIP so rate-limiting and checkOrigin defenses behave
consistently.
apps/coinblast/src/lib/origin.ts (1)

14-29: ⚡ Quick win

Document production-only origins in .env.example and ensure COINBLAST_ALLOWED_ORIGINS is properly configured.

The DEFAULT_ALLOWED includes localhost origins (3000-3002) intended for development, but COINBLAST_ALLOWED_ORIGINS is not documented in .env.example. While Caddy's TLS termination and routing in production means external browsers won't send localhost origins, it's a documentation gap and introduces risk if the reverse proxy is misconfigured. Add COINBLAST_ALLOWED_ORIGINS to .env.example with production-only origins (e.g., https://coinblast.sentriscloud.com), matching the approach used in the faucet app.

🤖 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/coinblast/src/lib/origin.ts` around lines 14 - 29, Add a
COINBLAST_ALLOWED_ORIGINS entry to .env.example documenting the production-only
origins (e.g., "https://coinblast.sentriscloud.com") and instructions that this
value overrides the DEFAULT_ALLOWED list used by the DEFAULT_ALLOWED constant
and parsed into the ALLOWED_ORIGINS Set in apps/coinblast/src/lib/origin.ts;
ensure the example emphasizes using only production origins (not localhost) and
shows the comma-separated format expected by the COINBLAST_ALLOWED_ORIGINS
environment variable.
🤖 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.

Inline comments:
In `@apps/coinblast/cb-deploy-7-tokens.mjs`:
- Around line 233-244: Replace the single 8s sleep + best-effort postMetadata
call with a retry loop that polls/post-retries with exponential backoff: after
creating the token event, repeatedly call postMetadata(launchClient, launchAcct,
curveAddr, imageUri, t.desc) until it succeeds or a max total timeout/attempts
is reached (use increasing delays between attempts), log each attempt and error,
and if the max timeout/attempts is exceeded throw/fail the script so deployment
does not exit silently with missing metadata; update references around the
existing console messages and the try/catch block to implement this behavior for
each token.

In `@apps/solux/src/components/Settings.tsx`:
- Around line 54-61: In Settings.tsx update the two clipboard-clear timeout
callbacks to check the current guard before calling
navigator.clipboard.writeText: in the seed timeout callback verify
lastCopiedSecret.current === mnemonic and only then call writeText('') and clear
lastCopiedSecret.current, and likewise in the key timeout callback verify
lastCopiedSecret.current === privateKey before calling writeText('') and
clearing the ref; this prevents an earlier timer from wiping the clipboard after
a different secret was copied.

---

Nitpick comments:
In `@apps/coinblast/src/lib/origin.ts`:
- Around line 14-29: Add a COINBLAST_ALLOWED_ORIGINS entry to .env.example
documenting the production-only origins (e.g.,
"https://coinblast.sentriscloud.com") and instructions that this value overrides
the DEFAULT_ALLOWED list used by the DEFAULT_ALLOWED constant and parsed into
the ALLOWED_ORIGINS Set in apps/coinblast/src/lib/origin.ts; ensure the example
emphasizes using only production origins (not localhost) and shows the
comma-separated format expected by the COINBLAST_ALLOWED_ORIGINS environment
variable.

In `@apps/coinblast/src/lib/rateLimit.ts`:
- Around line 59-70: getClientIP currently falls back to
X-Real-IP/X-Forwarded-For, allowing spoofing; change it so that in production it
requires CF-Connecting-IP only (do not fall back) and returns null (or the same
sentinel used by the faucet route) when CF-Connecting-IP is missing, while in
non-production keep the existing fallback behavior; update the getClientIP
function and align its return sentinel with the faucet route's getClientIP so
rate-limiting and checkOrigin defenses behave consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b1103b3b-13d8-4b5c-b682-0aebfc0967cd

📥 Commits

Reviewing files that changed from the base of the PR and between 931154c and c76e3d4.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (16)
  • .gitignore
  • apps/airdrop/src/components/ClaimWidget.tsx
  • apps/chain-landing/package.json
  • apps/coinblast/cb-deploy-7-tokens.mjs
  • apps/coinblast/src/app/api/cb/[...path]/route.ts
  • apps/coinblast/src/app/api/pin/route.ts
  • apps/coinblast/src/app/token/[address]/layout.tsx
  • apps/coinblast/src/components/token/BuySellWidget.tsx
  • apps/coinblast/src/lib/origin.ts
  • apps/coinblast/src/lib/rateLimit.ts
  • apps/dex/src/app/SwapWidget.tsx
  • apps/faucet/src/app/api/faucet/route.ts
  • apps/solux/.gitignore
  • apps/solux/package.json
  • apps/solux/src/components/Settings.tsx
  • package.json

Comment on lines +233 to +244
// Wait a few seconds for indexer to pick up the event before
// POSTing metadata (the indexer requires the row to exist first).
console.log('[meta] waiting 8s for indexer…')
await new Promise((r) => setTimeout(r, 8000))

console.log('[meta] POST owner-signed metadata')
try {
const metaResp = await postMetadata(launchClient, launchAcct, curveAddr, imageUri, t.desc)
console.log('[meta] →', metaResp)
} catch (e) {
console.log('[meta] ⚠ failed (will need retry):', e.message)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t make metadata publication a best-effort 8s race.

The script knows the indexer gate is eventual, but it waits once for 8 seconds and then swallows any failure. That makes rollout success timing-dependent and can leave tokens deployed without metadata while the script still exits cleanly. Retry with backoff until the row exists, and fail the run if a token never publishes.

Suggested patch
+async function postMetadataWithRetry(walletClient, account, curveAddress, imageUri, description) {
+  let delayMs = 2000
+  let lastError
+
+  for (let attempt = 1; attempt <= 6; attempt += 1) {
+    try {
+      return await postMetadata(walletClient, account, curveAddress, imageUri, description)
+    } catch (error) {
+      lastError = error
+      if (attempt === 6) break
+      console.log(`[meta] attempt ${attempt} failed, retrying in ${delayMs}ms: ${error.message}`)
+      await new Promise((resolve) => setTimeout(resolve, delayMs))
+      delayMs *= 2
+    }
+  }
+
+  throw lastError
+}
+
@@
-    // Wait a few seconds for indexer to pick up the event before
-    // POSTing metadata (the indexer requires the row to exist first).
-    console.log('[meta] waiting 8s for indexer…')
-    await new Promise((r) => setTimeout(r, 8000))
-
-    console.log('[meta] POST owner-signed metadata')
-    try {
-      const metaResp = await postMetadata(launchClient, launchAcct, curveAddr, imageUri, t.desc)
-      console.log('[meta] →', metaResp)
-    } catch (e) {
-      console.log('[meta] ⚠ failed (will need retry):', e.message)
-    }
+    console.log('[meta] POST owner-signed metadata')
+    const metaResp = await postMetadataWithRetry(
+      launchClient,
+      launchAcct,
+      curveAddr,
+      imageUri,
+      t.desc,
+    )
+    console.log('[meta] →', metaResp)
🤖 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/coinblast/cb-deploy-7-tokens.mjs` around lines 233 - 244, Replace the
single 8s sleep + best-effort postMetadata call with a retry loop that
polls/post-retries with exponential backoff: after creating the token event,
repeatedly call postMetadata(launchClient, launchAcct, curveAddr, imageUri,
t.desc) until it succeeds or a max total timeout/attempts is reached (use
increasing delays between attempts), log each attempt and error, and if the max
timeout/attempts is exceeded throw/fail the script so deployment does not exit
silently with missing metadata; update references around the existing console
messages and the try/catch block to implement this behavior for each token.

Comment on lines +54 to +61
useEffect(() => {
return () => {
if (lastCopiedSecret.current && navigator.clipboard) {
navigator.clipboard.writeText('').catch(() => {});
lastCopiedSecret.current = null;
}
};
}, []);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C2 "setTimeout\\(\\(\\) => \\{|writeText\\(''\\)|lastCopiedSecret\\.current ===|lastCopiedSecret\\.current !==" apps/solux/src/components/Settings.tsx

Repository: Sentriscloud/frontend

Length of output: 967


Move guard checks before writeText() to prevent unrelated clipboard clears.

In both timeout callbacks, writeText('') executes before verifying the secret is still current. If a user copies a different secret before the 60-second timer fires, the original timer will unnecessarily clear the clipboard anyway. Reorder each check to guard the write operation:

  • Lines 90–93 (seed timeout): Check lastCopiedSecret.current === mnemonic before calling writeText
  • Lines 153–156 (key timeout): Check lastCopiedSecret.current === privateKey before calling writeText
Proposed fix
     setTimeout(() => {
-      navigator.clipboard.writeText('').catch(() => {});
-      if (lastCopiedSecret.current === mnemonic) lastCopiedSecret.current = null;
+      if (lastCopiedSecret.current !== mnemonic) return;
+      lastCopiedSecret.current = null;
+      navigator.clipboard.writeText('').catch(() => {});
     }, 60_000);
     setTimeout(() => {
-      navigator.clipboard.writeText('').catch(() => {});
-      if (lastCopiedSecret.current === privateKey) lastCopiedSecret.current = null;
+      if (lastCopiedSecret.current !== privateKey) return;
+      lastCopiedSecret.current = null;
+      navigator.clipboard.writeText('').catch(() => {});
     }, 60_000);
🤖 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/solux/src/components/Settings.tsx` around lines 54 - 61, In Settings.tsx
update the two clipboard-clear timeout callbacks to check the current guard
before calling navigator.clipboard.writeText: in the seed timeout callback
verify lastCopiedSecret.current === mnemonic and only then call writeText('')
and clear lastCopiedSecret.current, and likewise in the key timeout callback
verify lastCopiedSecret.current === privateKey before calling writeText('') and
clearing the ref; this prevents an earlier timer from wiping the clipboard after
a different secret was copied.

@github-actions github-actions Bot merged commit 8098844 into main May 14, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant