Términos y Condiciones
MAIN_FRONT_SECURITY_AUDIT
main-front Security Audit
Date: 2026-05-12
Branch audited: 13-main-front (recently merged into dev via MR #13)
Scope: main-front/src — Next.js 14.1.0 marketplace frontend (~234 TS/TSX files, ~40k LOC)
Method: Trail of Bits audit-context-building skill (Phase 1 → Phase 2 → Phase 3), then supply-chain-risk-auditor + semgrep (important-only mode)
Tooling: Semgrep 1.157.0 OSS · 8 rulesets · merged SARIF in static_analysis_semgrep_1/
1. Executive Summary
The frontend is a Next.js 14 SPA acting as a marketplace UI for clients/professionals with chat, file uploads, blog/CMS, and MercadoPago-based subscription payments. A JWT in a non-HttpOnly cookie (with localStorage fallback) is the single auth credential for both HTTP API calls and STOMP-over-SockJS WebSocket connections. Authorization is delegated entirely to the backend; the Next.js middleware enforces cookie presence only — no signature/expiry/role verification.
Three areas concentrate the technical risk:
- Auth boundary is thin by design. Non-HttpOnly cookie + JWT decoded client-side + 7-day expiry + Bearer in
Authorizationheader means a single XSS payload exfiltrates a long-lived session. The codebase has threedangerouslySetInnerHTMLsinks (2 unsanitised) and onewindow.location.href = redirectUrlpropagated from backend without validation. - Payments take cardholder data through the SPA's own DOM rather than a MercadoPago-hosted iframe —
@mercadopago/sdk-*packages are declared inpackage.jsonbut never imported, and tokenisation is hand-rolled via a directfetchtoapi.mercadopago.com. A parallelAddCardFormflow posts raw PAN/CVV to the project's own backend. - Dependency manifest has integrity gaps.
next@14.1.0is pinned to a 2-year-old version vulnerable to CVE-2025-29927 (middleware authorisation bypass — the exact path used here as the auth gate).xlsx@0.18.5is the last npm-published SheetJS version; fixes for prototype-pollution and ReDoS live only on the SheetJS CDN.dompurify@3.3.3is present innode_modulesbut absent frompackage.jsonandpackage-lock.json—npm ciwill not reproduce it.
Static analysis (Semgrep, OSS, important-only filter) returned 3 findings, all the same react-dangerouslysetinnerhtml rule across the three sink locations. Two are true positives; one is a false positive (DOMPurify-wrapped). The remaining 11 hot-spots in this report were identified during context-building and require either custom Semgrep rules or manual fp-check to confirm; they are not yet classified as vulnerabilities.
Highest-priority remediations:
| P0 | Bump next to ^14.2.25 minimum (closes CVE-2025-29927) |
| P0 | Resolve the dompurify manifest gap (add to deps, or remove SafeHtml.tsx) |
| P1 | Sanitise blog block.html before dangerouslySetInnerHTML |
| P1 | Validate redirectUrl origin before window.location.href assignment |
| P1 | Escape </script> in JSON-LD payload on /pro/[slug]/[slug] |
| P1 | Migrate off xlsx@0.18.5 (CVEs unpatched on npm) |
| P1 | Gate MercadoPagoTest button behind an env flag |
Full action list in Section 9.
2. Scope and Methodology
2.1 In scope
main-front/src/— all TypeScript/TSX filesmain-front/package.json+main-front/package-lock.json— dependency manifestmain-front/next.config.js— runtime configuration (headers, rewrites)main-front/src/middleware.ts— Next.js edge middleware (auth gate)
2.2 Out of scope (this pass)
- Backend services (
core-module,chat-module,support-module,billing-module,notif-module,content-module) — referenced as trust boundaries but not analysed admin-front— separate service, different audit- Deployment / infrastructure (Docker, nginx, GitLab CI) — referenced as preconditions
- Mobile / PWA-specific behaviour (
MOBILE_AUDIT.mdis a separate document)
2.3 Method
Followed the Trail of Bits audit-context-building skill — three-phase approach:
- Phase 1 — System orientation. Mapped modules, entrypoints, actors, storage, sinks. Output: Section 3.
- Phase 2 — Ultra-granular function micro-analysis. Four parallel passes on selected fragility clusters: Auth/token, HTML rendering, Payments+redirects, WebSocket. Output: Section 4.
- Phase 3 — Global synthesis. Cross-cluster trust model, invariant reconstruction, fragility ranking. Output: Section 5.
Then hunting phase:
- Supply chain audit (Section 7) —
supply-chain-risk-auditoroverpackage.json+package-lock.json, results in .supply-chain-risk-auditor/results.md. - Static analysis (Section 6) — Semgrep 1.157.0 OSS, three parallel scan tasks, 8 rulesets. Outputs in static_analysis_semgrep_1/.
3. System Map
3.1 Actors and trust boundaries
| Actor | Trust | Touch points |
|---|---|---|
| Anonymous visitor | Untrusted | Public routes (/, /pro/*, /blog/*, /services/*), login/register, SEO endpoints (sitemap.ts, robots.ts) |
| Authenticated client | Semi-trusted | /(with-sidebar)/tasks, /(with-sidebar)/dashboard, chat, file uploads |
| Authenticated professional | Semi-trusted | /professional/*, payments, jobs, KYC/avatar moderation |
Backend API (/api/core/v1, /chat/v1, /support/v1, /billing/v1, /notif/v1) | Authoritative | Single shared origin via NEXT_PUBLIC_API_URL |
| MercadoPago | External 3rd party | SDK + iframe + return redirect |
| WebSocket / STOMP (SockJS fallback) | Backend channel | Chat, support, notifications |
3.2 Entrypoints
- Page routes (App Router): dozens of pages across
src/app/{(with-sidebar),blog,home,jobs,pro,professional,reset-password,services,simple,tasks,test,providers}/. Mix of server and client components. - API routes: only src/app/api/health/route.ts. Everything else proxies through to backend.
- Middleware: src/middleware.ts — gates
PROTECTED_PATHS/PROFESSIONAL_ONLY_PATHS/CLIENT_ONLY_PATHSbyauth_tokencookie presence and injectsX-Forwarded-Forfor/api/*and/ws/*. - WebSocket clients: three contexts — websocket-context.tsx (main chat), support-websocket-context.tsx (support), unread-messages-context.tsx (consumer of the main channel).
3.3 Storage and state
| Storage | Where | Notes |
|---|---|---|
auth_token cookie | utils/auth.ts via js-cookie; secure: NODE_ENV==='production', sameSite: 'lax' for Safari else 'strict', domain: hostname, path: '/', expires: 7 days | Read also by middleware (middleware.ts:94). NOT HttpOnly by design (JS must read it for Authorization header). |
localStorage['auth_token'] | Fallback in setAuthToken if the primary cookie write fails verification (Safari ITP path) | Fallback drops secure: false |
localStorage / sessionStorage (other) | 48 mentions in src/** | Content not enumerated in this audit |
| In-memory React state | AuthContext.user — decoded from JWT via jwtDecode (auth-context.tsx:52) | UI display only; not authoritative |
3.4 API surface
- 27 API client modules under src/api/.
- Common fetch wrappers in src/lib/api.ts — 5 sibling variants (
fetchApi/fetchChatApi/fetchSupportApi/fetchBillingApi/fetchNotifApi) that differ only in service prefix; identical auth/locale/Content-Type/401 handling. - 401 handling centralised in src/lib/api-interceptor.ts via a module-singleton
unauthorizedHandlercallback registered byAuthProvider.
3.5 Sinks of interest (catalogued, not classified)
| Class | Locations |
|---|---|
dangerouslySetInnerHTML | SafeHtml.tsx:36 (DOMPurify-wrapped), pro/[categorySlug]/[slug]/page.tsx:194 (JSON-LD), BlogDetailPage.tsx:101 (raw block.html) |
Dynamic window.location.href = … | mercado-pago-card-form.tsx:143 (redirectUrl from backend response), several hard-coded literals elsewhere |
eval / new Function | Absent |
postMessage listeners | Absent |
Server-only process.env.* | INTERNAL_API_URL in pro/[categorySlug]/[slug]/page.tsx:19 and sitemap.ts:7 |
4. Per-cluster context
4.1 Auth / token cluster
Trust model. The cluster issues a backend-signed JWT, persists it as a non-HttpOnly cookie + localStorage fallback, and bridges three trust zones:
- Server middleware (middleware.ts) reads the cookie for route gating and rewrites
X-Forwarded-Forfor/api/*and/ws/*proxying. - Client React (auth-context.tsx, utils/auth.ts) reads/writes the cookie via
js-cookie, decodes the JWT withjwt-decode(no signature verification), and reacts to 401 via a module-singleton handler. - Outbound API callers (lib/api.ts, api/auth.ts) attach
Bearer <token>from the cookie.
The cookie is not HttpOnly by design: JS must read it both for Authorization headers and for the middleware's design. The middleware's cookie check is a UX gate (presence only — no signature/expiry validation). The JWT is decoded client-side only for display fields and an exp check.
Cookie flag map (from setAuthToken + getSafariCompatibleCookieOptions in browser-detection.ts:62-76):
- Primary path:
expires: 7d,path: '/',sameSite: 'lax'(Safari) /'strict',secure: production-only,domain: window.location.hostname(no leading dot → host-only),HttpOnly: not set. - Safari fallback path (utils/auth.ts:44-73): same but
secure: false, no domain. - Removal path (utils/auth.ts:82-98):
Cookies.remove(..., { path: '/' })—domainis not specified; potential mismatch with the setter when production cookies were set withdomain: hostname.
Key flow elements:
validateAndDecodeToken(auth-context.tsx:46-69) — decode viajwtDecode(no signature check), reject ifexpelapsed, clear storage on expiry or decode error. Tokens withoutexpare treated as valid.loadUserProfile(auth-context.tsx:71-205) — HEAD probe to/core/v1/shared/categoriesfor backend health, role-branched profile load, 1-secondsetTimeoutfor online-status ping with re-validation of token+sub.setUnauthorizedHandlereffect (auth-context.tsx:256-280) — module-singleton callback; last writer wins; no cleanup on unmount.fetchXxxApi× 5 (lib/api.ts) — every authenticated call funnels throughhandleApiResponse, ensuring uniform 401 handling.
Hot-spots requiring further investigation:
- The 1-second timer race in
loadUserProfile/login— control flow enumerated; the timer re-reads the token and matches bysub, mitigating most scenarios. The remaining risk path is localStorage staying populated after a swallowed cookie-removal exception. - Cookie domain mismatch on removal.
- Safari fallback writes the cookie with
secure: false. - JWT without
expclaim is not rejected. X-Forwarded-Foris read fromrequest.headers['x-forwarded-for'][0]without trusted-proxy allowlist. Correctness depends on a deployment-level guarantee that an upstream proxy strips/rewrites the header before reaching Next.js.
4.2 HTML rendering cluster
Three dangerouslySetInnerHTML sinks:
| Location | Source | Mode | Sanitiser |
|---|---|---|---|
| SafeHtml.tsx:36 | html prop (KB content from backend) | 'use client'; SSR + CSR | DOMPurify.sanitize with explicit PURIFY_CONFIG |
| BlogDetailPage.tsx:101 | block.html from BlogContentBlock; post.content[locale] fetched from GET ${APP_URL}/api/content/v1/public/blog/{slug} | 'use client'; SSR + CSR | None (raw passthrough) |
| pro/[categorySlug]/[slug]/page.tsx:194 | JSON.stringify(jsonLd) with backend-supplied fields (professionalName, subHeadline, professionalDescription, etc.) | Server component; SSR | None — content is JSON inside <script type="application/ld+json">; </script> not escaped |
DOMPurify config in ****SafeHtml.tsx allows: structural tags (h1-h6, p, a, lists, tables, headings, code, blockquote, img, figure/figcaption); attributes href, src, alt, title, class, id, target, rel, width, height; ALLOW_DATA_ATTR: false. Excluded: <script>, <style>, <iframe>, <svg>, <math>, <form>/<input>, <object>/<embed>, <link>/<meta>, <video>/<audio>, <canvas>, <details>. Missing hardening: no afterSanitizeAttributes hook to force rel="noopener noreferrer" when target="_blank"; SANITIZE_NAMED_PROPS unset (DOM clobbering surface via id/name attributes).
Critical anomalies found during analysis:
dompurify@3.3.3is present innode_modules/but absent frompackage.jsonand ****package-lock.json. The only file referencing it (SafeHtml.tsx) is in an untracked directory (?? src/components/help/). On a cleannpm cibuild, the dependency disappears.SafeHtml.tsxhas zero importers at HEAD — confirmed dead code, WIP for the Knowledge Base feature. The semgrepdangerouslySetInnerHTMLfinding on this file is therefore a true positive for the rule but a false positive for actual exploitability today.JSON.stringifyatpage.tsx:194does not escape</script>or<!--. The standard mitigation is.replace(/<\/script/gi, '<\\/script')before embedding inside<script>. Not implemented.BlogDetailPageis the live, unsanitised XSS sink. Its security depends entirely on whethercontent-modulesanitises HTML at write time. Not verifiable from frontend code alone.
4.3 Payments + redirects (MercadoPago) cluster
Payment flow:
SubscriptionPage → handlePlanClick(plan)
→ Dialog mounts <MercadoPagoCardForm>
→ onMount: GET /billing/v1/mercado-pago/public-key → { publicKey }
→ User types PAN/CVV in <Input>/<MaskedInput> (NOT a MercadoPago iframe)
→ onSubmit:
POST api.mercadopago.com/v1/card_tokens?public_key=... → { id }
POST /billing/v1/subscription with { cardTokenId, ... }
→ MercadoPagoPaymentResponse { id, status, initPoint?, sandboxInitPoint?, externalReference }
redirectUrl = sandboxInitPoint || initPoint
if (redirectUrl) { window.location.href = redirectUrl; return; }
else { status === 'authorized'|'pending' → onSuccess; otherwise throw }
Key observations from the cluster (Phase 2):
- MercadoPago SDKs are dead deps.
@mercadopago/sdk-js@^0.0.3and@mercadopago/sdk-react@^1.0.7are declared inpackage.jsonbutgrepreturns zero imports insrc/. The integration is hand-rolled via rawfetchtoapi.mercadopago.com/v1/card_tokens. NoinitMercadoPago(...)call, no SDK iframe — cardholder data lives in the SPA's own DOM during the form lifetime. redirectUrl**** propagation has no validation. Backend response →handleApiResponse(pass-through) →fetchBillingApi(pass-through) →createSubscriptionPaymentEnhanced(presence checks onexternalReferenceandidonly) →onSubmit(single truthy check onredirectUrl) →window.location.href. Nonew URL(...)parse, no scheme check, no host allowlist.- A parallel path posts raw PAN/CVV to the project's own backend. add-card-form.tsx:66-79 POSTs
cardNumber, cvc, expMonth, expYear, cardholderNamedirectly to/billing/v1/payment-methods. Whether the backend then tokenises and discards, or stores raw card data, is out of frontend scope but needs backend confirmation. MercadoPagoTest**** is reachable in production via the "Show Mercado Pago Test" toggle in subscription-page.tsx:622-637. It callscreateSubscriptionPaymentEnhancedwith hard-codedplanId: '1',payerEmail: 'test@example.com'. Backend rejection in prod is unverified.- No
back_url/success_url/failure_urlconfiguration in the frontend. Return URLs must be configured server-side. The frontend's/professional/subscription/payment-successroute is a no-op thatrouter.replace('/professional/dashboard'). - No CSP in next.config.js:80-117 — only
X-Frame-Options: SAMEORIGIN. MercadoPagoCardFormacceptsamount,description,externalReference**** props but never reads them (mercado-pago-card-form.tsx:86-92). The UI and the backend price the plan independently — drift would not be detected here.
4.4 WebSocket cluster
Three contexts, same auth artifact (JWT):
| Context | Transport | URL | Auth | Subscribes to |
|---|---|---|---|---|
| websocket-context.tsx | STOMP/SockJS (fallback chain: websocket, xhr-streaming, xhr-polling) | /api/chat/v1/ws/ | STOMP connectHeaders.Authorization: Bearer <token> | /topic/{userId}, /topic/read/{userId}, /topic/user-status (global), /user/queue/upload-progress, /user/queue/upload-complete, /user/{userId}/notifications, dynamic /topic/conversation/{conversationId}/typing |
| support-websocket-context.tsx | STOMP/SockJS | /api/support/v1/ws/ | Same | /topic/support/user/{userId} |
| unread-messages-context.tsx | n/a — pure consumer | n/a | n/a | Listener registered against the main context |
userId in topic names = user.sub ?? user.id, both derived from the locally-decoded JWT. Client-controlled string in the destination — broker-side ACL is the only gate against subscribing to another user's topic. /topic/{userId} is a plain topic destination (not the STOMP /user/ per-session prefix), so server-side enforcement is mandatory.
Token lifecycle on the WS channel:
@stomp/stompjsauto-reconnects withreconnectDelay: 3000ms(chat) /5000ms(support).- The
connectHeaders.Authorizationvalue is captured at the time of ****connect(). On reconnect, the same captured token is re-used. There is no token-rotation listener; a silently-refreshed token in the cookie would not propagate to STOMP until auser.subchange forces adisconnect()+ newconnect().
Payload trust: every inbound STOMP message is JSON.parse'd and cast with TypeScript as to the expected interface. No runtime schema validation. senderId, targetUserId, systemMessageType are trusted as-is by ChatComponent.tsx (handleIncomingMessage around lines 1620-1822) and by UnreadMessagesProvider.
Notable cross-cluster paths:
WebNotification.data.deepLinkUrl→router.push(deepLinkUrl)in NotificationBell.tsx:150-156. Next router blocksjavascript:schemes but accepts any same-origin path.WebNotification.data.icon→ browserNotification(title, { body, icon })constructor. Icon URL not validated.- No path observed from WS payload to
dangerouslySetInnerHTML— verified by full grep.
Orphan code in the WS stack:
socket.io-client@^4.8.1+@types/socket.io-client@^1.4.36declared inpackage.jsonbut never imported insrc/.useStompWebSocket.tshook is unused (no consumers).<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js" defer>in layout.tsx:86 loads a CDN copy of SockJS into the document head, but every code path importssockjs-clientas an npm dep — the CDN script is dead weight (and a third-party CDN dependency without SRI).- Middleware's
/ws/*branch (middleware.ts:65) is unreachable: the WS URL starts with/api/, so the/api/*branch matches first.
5. Cross-cluster synthesis
5.1 Unified trust diagram
Anonymous request ─┐
├─→ Next.js Edge Middleware (presence-only auth gate)
Authenticated user ─┘ │
│ /api/* & /ws/* → rewrite X-Forwarded-For
│ page route → cookie present? else redirect ?showLogin=true
▼
SPA (auth_token cookie, NOT HttpOnly, secure=prod-only, sameSite=lax/strict,
expires=7d, path=/, domain=hostname; localStorage fallback)
│
jwtDecode (no signature check) → React user state
│
┌─────────────────┬────┴───────────┬───────────────────┐
▼ ▼ ▼ ▼
HTTP API call STOMP WS connect dangerouslySet window.location.href
(Bearer) (Bearer in InnerHTML sinks (redirectUrl from backend)
connectHeaders, (3 sinks; only
captured at SafeHtml — dead ┌─ MercadoPago direct
connect, never code — uses tokenization (PAN/CVV
refreshed) DOMPurify) in SPA inputs)
│ │ │ │
▼ ▼ ▼ ▼
Backend ◄────── Backend ──────► Backend HTML External (MP) /
(authoritative) (sole authz backend (initPoint)
gate on
topic scope)
5.2 Invariants
- The JWT in
auth_tokenis the sole auth credential for HTTPAuthorization: Bearer, WebSocketconnectHeaders.Authorization, and middleware presence check. - Backend is the only authorisation authority. Middleware checks cookie presence, not signature/expiry/role.
- No runtime payload validation anywhere downstream of the network: WS payloads (
ascast),ResponseDto.result(as T),mapPublicResponse(untypedRecord<string, unknown>+ casts),BlogContentBlocktypes are assumed. - No CSP header configured (next.config.js:80-117) — only
X-Frame-Options: SAMEORIGIN. - Cookie is not HttpOnly by design — required by JS for
Authorizationheader. Trade-off: XSS exfiltrates a 7-day session. - Five
fetchApivariants differ only in service prefix; identical 401 funnel throughhandleApiResponse→handleUnauthorizedResponse→setUnauthorizedHandlercallback. dompurifyis not declared inpackage.json/package-lock.json— present innode_modulesv3.3.3 only becausenpm i --no-savewas run locally.- Token captured at WS connect — STOMP auto-reconnect re-uses the same token variable; no refresh path.
- JWT signature is never verified client-side. No JWKS endpoint is referenced.
5.3 Trust boundary map (consolidated)
| Boundary | Source → Sink | Trust gate observed in frontend code |
|---|---|---|
| Anonymous user → public blog HTML | content-module → BlogDetailPage dangerouslySetInnerHTML | None — raw passthrough |
| Anonymous user → public pro landing | content-module → JSON.stringify in <script type=application/ld+json> | None — </script> not escaped |
Backend → redirectUrl | /billing/v1/subscription response → window.location.href | Truthy check only |
| MercadoPago → tokenisation payload | api.mercadopago.com/v1/card_tokens | Direct browser POST; PAN in SPA DOM |
| Network → middleware XFF | x-forwarded-for[0] → backend X-Forwarded-For | No upstream proxy verification in code |
| WebSocket → UI state | STOMP frame → WebSocketMessage.senderId / data | Only JSON.parse + try/catch |
| WebSocket → navigation | WebNotification.data.deepLinkUrl → router.push() | Next router blocks javascript: but accepts any same-origin path |
6. Findings inventory
6.1 Semgrep (Important-only, 3 hits)
| # | File:Line | Rule | Severity / Confidence / Impact | Classification |
|---|---|---|---|---|
| 1 | BlogDetailPage.tsx:101 | react-dangerouslysetinnerhtml | WARNING / MEDIUM / MEDIUM | TRUE POSITIVE — raw block.html from backend; no defence-in-depth on the frontend |
| 2 | pro/[categorySlug]/[slug]/page.tsx:194 | react-dangerouslysetinnerhtml | WARNING / MEDIUM / MEDIUM | TRUE POSITIVE — JSON-LD payload, </script> not escaped; backend-supplied professional fields |
| 3 | SafeHtml.tsx:36 | react-dangerouslysetinnerhtml | WARNING / MEDIUM / MEDIUM | FALSE POSITIVE (today) — DOMPurify-wrapped; but the component has zero importers and dompurify is not in the manifest |
6.2 Manual hot-spots (context-building, not classified)
These were surfaced during Phase 2/3 but do not have matching Semgrep rules in the OSS rule packs. They require either custom Semgrep rules or manual fp-check to graduate to "finding":
| # | Location | Class | Mitigation owner | Source |
|---|---|---|---|---|
| 1 | mercado-pago-card-form.tsx:140-143 — window.location.href = redirectUrl without scheme/host validation | Open redirect | Frontend (add allowlist) or backend (enforce MP host) | §4.3 |
| 2 | add-card-form.tsx:66-79 — raw PAN/CVV → /billing/v1/payment-methods | PCI scope creep | Backend (must tokenise, not store); frontend can switch to MP iframe | §4.3 |
| 3 | utils/auth.ts:32-80 — Bearer token in non-HttpOnly cookie + localStorage fallback, 7d expiry | XSS exfiltration target | Trade-off accepted; mitigations are CSP + reducing XSS sinks | §4.1 |
| 4 | utils/auth.ts:44-73 — Safari fallback writes cookie with secure: false | Cleartext token over HTTP if site ever served plaintext | Frontend (probe with secure: true, sameSite: 'lax' before downgrading) | §4.1 |
| 5 | auth-context.tsx:52-56 — JWT without exp claim is not rejected | Indefinite session | Frontend (require exp); backend should always issue | §4.1 |
| 6 | middleware.ts:65-82 — X-Forwarded-For injection without trusted-proxy allowlist | IP spoofing into backend audit / rate-limit | Deployment (trusted edge proxy) | §4.1 |
| 7 | websocket-context.tsx:160-166 — STOMP captures token in closure; reconnect uses stale token | Stale auth on token rotation | Frontend (re-read getAuthToken() in webSocketFactory) | §4.4 |
| 8 | websocket-context.tsx:199-302 — client-built topics /topic/{userId} + UI trust in payload senderId/targetUserId | Authz delegated 100% to broker | Backend (broker ACL on SUBSCRIBE) | §4.4 |
| 9 | NotificationBell.tsx:150-156 — router.push(deepLinkUrl) from WS payload | Internal navigation tampering | Backend (constrain deepLinkUrl to same-origin paths) | §4.4 |
| 10 | subscription-page.tsx:622-637 — MercadoPagoTest toggle reachable in prod | Test endpoint exposure | Frontend (env-flag) and backend (reject test payloads in prod) | §4.3 |
| 11 | next.config.js — no CSP, only X-Frame-Options: SAMEORIGIN | No defence-in-depth | Frontend (add CSP header config) | §4.3, §5 |
| 12 | auth.ts:82-98 — removeAuthToken omits domain when setter used domain: hostname | Possible cookie persistence after logout | Frontend (mirror set/remove flags) | §4.1 |
6.3 Out-of-scope but flagged for follow-up
- 48
localStorage/sessionStoragementions insrc/**not catalogued in this audit. Recommend a separate pass to inventory PII/token content. - File-upload pipelines (
xlsx,sharp,react-image-crop) — not deep-dived. See Section 7 for dependency-level concerns. - Untracked WIP files (legal/, help/, kb-api, public-strings) — semgrep scanned what was on disk but the surface will change before MR.
7. Supply chain risk
Full report in .supply-chain-risk-auditor/results.md. Summary:
7.1 High-risk dependencies
| Dependency | Pinned / installed / latest | Risk | Note |
|---|---|---|---|
next | 14.1.0 / 14.1.0 / 16.2.6 | CVEs, 2-year-old major | Pinned exact (not ^). Vulnerable to CVE-2025-29927 (middleware auth bypass, fix in 14.2.25). The middleware is the auth gate. |
xlsx | ^0.18.5 / 0.18.5 / 0.18.5 on npm | Single maintainer, unmaintained on npm, CVEs | SheetJS removed xlsx from npm in 2023. Prototype pollution (GHSA-4r6h-8v6p-xvw6) and ReDoS (GHSA-5pgg-2g8v-p4x9) fixes ship only at 0.20.2+ on the SheetJS CDN. |
axios | ^1.9.0 / 1.12.2 / 1.16.0 | Version drift, CVEs | Within semver range; just refresh the lockfile. |
sharp | ^0.33.5 / 0.33.x / 0.34.5 | Single maintainer (well-known), FFI to libvips | Bump to ^0.34.x. |
sockjs-client | ^1.6.1 | Single maintainer, unmaintained (last publish Oct 2022) | Use native WebSocket via @stomp/stompjs brokerURL. |
@types/socket.io-client | ^1.4.36 | Deprecated (2022) | Remove. |
socket.io-client | ^4.8.1 | Dead dep (zero imports) | Remove. |
@mercadopago/sdk-js | ^0.0.3 | Pre-1.0 version, dead dep | Remove or actually adopt for iframe-isolated tokenisation. |
@mercadopago/sdk-react | ^1.0.7 | Dead dep | Remove or adopt. |
7.2 Manifest integrity
dompurify@3.3.3 is present in node_modules but absent from both package.json and package-lock.json. Single-command fix: npm i dompurify@^3.4.2 -E. Without it, a clean npm ci will drop the dependency and SafeHtml.tsx either fails to build or silently loses sanitisation. Currently the component has zero importers, so the latent bug isn't observable — until the KB feature ships.
8. Disconfirmation / open questions
These require backend verification or a separate investigation pass and are not classified as findings in this report:
- CMS sanitisation for blog ****
block.html— doescontent-moduleaccept arbitrary HTML on write, or constrain via a WYSIWYG? Who can author (admin / professional / open registration)? - STOMP broker ACL — does the broker reject
SUBSCRIBE /topic/{otherUserId}from a session authenticated asuserId? - MP
initPointorigin enforcement — doesbilling-modulecopy MercadoPago'sinitPointverbatim, or transform it? Could a backend bug substitute an attacker-controlled URL? exp**** claim guarantee — does the backend always issue JWTs withexp?- Trusted edge proxy — does the deployment topology guarantee that
x-forwarded-forreaching Next.js has been stripped/rewritten by a trusted upstream? AddCardForm**** backend behaviour — does/billing/v1/payment-methodstokenise and discard, or store raw PAN/CVV? If the latter, PCI scope is materially different./api/kb/***** routing — no Next.js rewrite is configured (next.config.js:133-156); does nginx proxy this in prod?- MP CheckoutPro return URLs —
back_url/success_url/failure_urlnot configured in the frontend. Where does backend configure them?
9. Prioritized action plan
| Prio | Action | File / scope | Effort |
|---|---|---|---|
| P0 | Bump next to ^14.2.25 minimum (CVE-2025-29927) | main-front/package.json | ~30 min + smoke test |
| P0 | Resolve dompurify manifest gap — either npm i dompurify@^3.4.2 -E or remove SafeHtml.tsx (currently has zero importers) | main-front/package.json, main-front/src/components/help/ | 5 min |
| P1 | Sanitise block.html in BlogDetailPage (DOMPurify) or verify+document backend sanitisation in CMS | BlogDetailPage.tsx:99-103 | 2 h |
| P1 | Escape </script> and <!-- in JSON-LD payload | pro/[categorySlug]/[slug]/page.tsx:194 | 15 min |
| P1 | Add host allowlist for redirectUrl before window.location.href (new URL(redirectUrl).host ∈ MP hosts) | mercado-pago-card-form.tsx:140-143 | 1 h |
| P1 | Gate MercadoPagoTest toggle behind process.env.NEXT_PUBLIC_ENABLE_MP_TEST (or remove the test component) | subscription-page.tsx:622-637 | 30 min |
| P1 | Migrate off xlsx@0.18.5 (CVEs unpatched on npm) — exceljs or SheetJS CDN tarball | main-front/src/utils/__tests__/ callers; export features | 4–8 h |
| P1 | Bump axios to 1.16.0 within ^1.9.0 range | main-front/package-lock.json | 5 min |
| P2 | Add CSP header in next.config.js (start with default-src 'self'; frame-src https://*.mercadopago.com; connect-src 'self' https://api.mercadopago.com) | next.config.js | 2–4 h (testing) |
| P2 | Strict role check in middleware — currently only verifies presence; should verify role from a server-trusted source | middleware.ts:94-106 | 2 h |
| P2 | Require JWT exp claim in validateAndDecodeToken (reject tokens without exp) | auth-context.tsx:46-69 | 15 min |
| P2 | WS token refresh — re-read getAuthToken() in webSocketFactory so STOMP reconnects use a fresh token | websocket-context.tsx:160-166 | 1 h |
| P2 | Remove dead deps: @mercadopago/sdk-js, @mercadopago/sdk-react, socket.io-client, @types/socket.io-client | main-front/package.json | 30 min + npx depcheck validation |
| P2 | Remove unused CDN sockjs.min.js script from layout.tsx | layout.tsx:86 | 5 min |
| P2 | Fix cookie domain mismatch on removal — mirror set flags in Cookies.remove(..., { path, domain }) | utils/auth.ts:82-98 | 30 min |
| P3 | Plan sockjs-client removal — native WebSocket via @stomp/stompjs brokerURL | websocket-context.tsx:161-163 | 2 h |
| P3 | Runtime validation (zod) on WS payloads (WebSocketMessage, ReadNotification, WebNotification, MessageResponse) | websocket-context.tsx and consumers | 4 h |
| P3 | DRY the 5 fetchApi copies into one helper parameterised by service prefix | lib/api.ts | 2 h |
| P3 | X-Forwarded-For trusted-proxy allowlist (requires deployment coordination) | middleware.ts:65-82 | ops-dependent |
| P3 | Setup npm audit in GitLab CI to catch future regressions | .gitlab-ci.yml | 30 min |
| P3 | Inventory 48 localStorage / sessionStorage call sites; document what's stored | main-front/src/** | 4 h |
10. Artifacts
- .supply-chain-risk-auditor/results.md — full supply-chain report
- static_analysis_semgrep_1/results/results.sarif — merged SARIF (3 findings)
- static_analysis_semgrep_1/raw/ — per-ruleset JSON + SARIF (8 files)
- static_analysis_semgrep_1/rulesets.txt — approved rulesets log
- static_analysis_semgrep_1/repos/semgrep-rules/ — Trail of Bits semgrep-rules clone (shallow)
11. Methodology references
- Trail of Bits
audit-context-buildingskill - Trail of Bits
supply-chain-risk-auditorskill - Trail of Bits
semgrepskill - Semgrep registry —
p/security-audit,p/secrets,p/owasp-top-ten,p/typescript,p/react,p/nextjs - Trail of Bits semgrep-rules — github.com/trailofbits/semgrep-rules
Generated 2026-05-12 by audit pipeline. See git log for any subsequent updates.