header.blog

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:

  1. Auth boundary is thin by design. Non-HttpOnly cookie + JWT decoded client-side + 7-day expiry + Bearer in Authorization header means a single XSS payload exfiltrates a long-lived session. The codebase has three dangerouslySetInnerHTML sinks (2 unsanitised) and one window.location.href = redirectUrl propagated from backend without validation.
  2. Payments take cardholder data through the SPA's own DOM rather than a MercadoPago-hosted iframe — @mercadopago/sdk-* packages are declared in package.json but never imported, and tokenisation is hand-rolled via a direct fetch to api.mercadopago.com. A parallel AddCardForm flow posts raw PAN/CVV to the project's own backend.
  3. Dependency manifest has integrity gaps. next@14.1.0 is 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.5 is the last npm-published SheetJS version; fixes for prototype-pollution and ReDoS live only on the SheetJS CDN. dompurify@3.3.3 is present in node_modules but absent from package.json and package-lock.json — npm ci will 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 files
  • main-front/package.json + main-front/package-lock.json — dependency manifest
  • main-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.md is a separate document)

2.3 Method

Followed the Trail of Bits audit-context-building skill — three-phase approach:

  1. Phase 1 — System orientation. Mapped modules, entrypoints, actors, storage, sinks. Output: Section 3.
  2. Phase 2 — Ultra-granular function micro-analysis. Four parallel passes on selected fragility clusters: Auth/token, HTML rendering, Payments+redirects, WebSocket. Output: Section 4.
  3. Phase 3 — Global synthesis. Cross-cluster trust model, invariant reconstruction, fragility ranking. Output: Section 5.

Then hunting phase:

  1. Supply chain audit (Section 7) — supply-chain-risk-auditor over package.json + package-lock.json, results in .supply-chain-risk-auditor/results.md.
  2. 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

ActorTrustTouch points
Anonymous visitorUntrustedPublic routes (/, /pro/*, /blog/*, /services/*), login/register, SEO endpoints (sitemap.ts, robots.ts)
Authenticated clientSemi-trusted/(with-sidebar)/tasks, /(with-sidebar)/dashboard, chat, file uploads
Authenticated professionalSemi-trusted/professional/*, payments, jobs, KYC/avatar moderation
Backend API (/api/core/v1, /chat/v1, /support/v1, /billing/v1, /notif/v1)AuthoritativeSingle shared origin via NEXT_PUBLIC_API_URL
MercadoPagoExternal 3rd partySDK + iframe + return redirect
WebSocket / STOMP (SockJS fallback)Backend channelChat, 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_PATHS by auth_token cookie presence and injects X-Forwarded-For for /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

StorageWhereNotes
auth_token cookieutils/auth.ts via js-cookie; secure: NODE_ENV==='production', sameSite: 'lax' for Safari else 'strict', domain: hostname, path: '/', expires: 7 daysRead 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 stateAuthContext.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 unauthorizedHandler callback registered by AuthProvider.

3.5 Sinks of interest (catalogued, not classified)

ClassLocations
dangerouslySetInnerHTMLSafeHtml.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 FunctionAbsent
postMessage listenersAbsent
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:

  1. Server middleware (middleware.ts) reads the cookie for route gating and rewrites X-Forwarded-For for /api/* and /ws/* proxying.
  2. Client React (auth-context.tsx, utils/auth.ts) reads/writes the cookie via js-cookie, decodes the JWT with jwt-decode (no signature verification), and reacts to 401 via a module-singleton handler.
  3. 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: '/' }) — domain is not specified; potential mismatch with the setter when production cookies were set with domain: hostname.

Key flow elements:

  • validateAndDecodeToken (auth-context.tsx:46-69) — decode via jwtDecode (no signature check), reject if exp elapsed, clear storage on expiry or decode error. Tokens without exp are treated as valid.
  • loadUserProfile (auth-context.tsx:71-205) — HEAD probe to /core/v1/shared/categories for backend health, role-branched profile load, 1-second setTimeout for online-status ping with re-validation of token+sub.
  • setUnauthorizedHandler effect (auth-context.tsx:256-280) — module-singleton callback; last writer wins; no cleanup on unmount.
  • fetchXxxApi × 5 (lib/api.ts) — every authenticated call funnels through handleApiResponse, 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 by sub, 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 exp claim is not rejected.
  • X-Forwarded-For is read from request.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:

LocationSourceModeSanitiser
SafeHtml.tsx:36html prop (KB content from backend)'use client'; SSR + CSRDOMPurify.sanitize with explicit PURIFY_CONFIG
BlogDetailPage.tsx:101block.html from BlogContentBlock; post.content[locale] fetched from GET ${APP_URL}/api/content/v1/public/blog/{slug}'use client'; SSR + CSRNone (raw passthrough)
pro/[categorySlug]/[slug]/page.tsx:194JSON.stringify(jsonLd) with backend-supplied fields (professionalName, subHeadline, professionalDescription, etc.)Server component; SSRNone — 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.3 is present in node_modules/ but absent from package.json and ****package-lock.json. The only file referencing it (SafeHtml.tsx) is in an untracked directory (?? src/components/help/). On a clean npm ci build, the dependency disappears.
  • SafeHtml.tsx has zero importers at HEAD — confirmed dead code, WIP for the Knowledge Base feature. The semgrep dangerouslySetInnerHTML finding on this file is therefore a true positive for the rule but a false positive for actual exploitability today.
  • JSON.stringify at page.tsx:194 does not escape </script> or <!--. The standard mitigation is .replace(/<\/script/gi, '<\\/script') before embedding inside <script>. Not implemented.
  • BlogDetailPage is the live, unsanitised XSS sink. Its security depends entirely on whether content-module sanitises 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):

  1. MercadoPago SDKs are dead deps. @mercadopago/sdk-js@^0.0.3 and @mercadopago/sdk-react@^1.0.7 are declared in package.json but grep returns zero imports in src/. The integration is hand-rolled via raw fetch to api.mercadopago.com/v1/card_tokens. No initMercadoPago(...) call, no SDK iframe — cardholder data lives in the SPA's own DOM during the form lifetime.
  2. redirectUrl**** propagation has no validation. Backend response → handleApiResponse (pass-through) → fetchBillingApi (pass-through) → createSubscriptionPaymentEnhanced (presence checks on externalReference and id only) → onSubmit (single truthy check on redirectUrl) → window.location.href. No new URL(...) parse, no scheme check, no host allowlist.
  3. A parallel path posts raw PAN/CVV to the project's own backend. add-card-form.tsx:66-79 POSTs cardNumber, cvc, expMonth, expYear, cardholderName directly 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.
  4. MercadoPagoTest**** is reachable in production via the "Show Mercado Pago Test" toggle in subscription-page.tsx:622-637. It calls createSubscriptionPaymentEnhanced with hard-coded planId: '1', payerEmail: 'test@example.com'. Backend rejection in prod is unverified.
  5. No back_url / success_url / failure_url configuration in the frontend. Return URLs must be configured server-side. The frontend's /professional/subscription/payment-success route is a no-op that router.replace('/professional/dashboard').
  6. No CSP in next.config.js:80-117 — only X-Frame-Options: SAMEORIGIN.
  7. MercadoPagoCardForm accepts amount, 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):

ContextTransportURLAuthSubscribes to
websocket-context.tsxSTOMP/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.tsxSTOMP/SockJS/api/support/v1/ws/Same/topic/support/user/{userId}
unread-messages-context.tsxn/a — pure consumern/an/aListener 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/stompjs auto-reconnects with reconnectDelay: 3000ms (chat) / 5000ms (support).
  • The connectHeaders.Authorization value 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 a user.sub change forces a disconnect() + new connect().

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 blocks javascript: schemes but accepts any same-origin path.
  • WebNotification.data.icon → browser Notification(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.36 declared in package.json but never imported in src/.
  • useStompWebSocket.ts hook 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 imports sockjs-client as 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

  1. The JWT in auth_token is the sole auth credential for HTTP Authorization: Bearer, WebSocket connectHeaders.Authorization, and middleware presence check.
  2. Backend is the only authorisation authority. Middleware checks cookie presence, not signature/expiry/role.
  3. No runtime payload validation anywhere downstream of the network: WS payloads (as cast), ResponseDto.result (as T), mapPublicResponse (untyped Record<string, unknown> + casts), BlogContentBlock types are assumed.
  4. No CSP header configured (next.config.js:80-117) — only X-Frame-Options: SAMEORIGIN.
  5. Cookie is not HttpOnly by design — required by JS for Authorization header. Trade-off: XSS exfiltrates a 7-day session.
  6. Five fetchApi variants differ only in service prefix; identical 401 funnel through handleApiResponse → handleUnauthorizedResponse → setUnauthorizedHandler callback.
  7. dompurify is not declared in package.json / package-lock.json — present in node_modules v3.3.3 only because npm i --no-save was run locally.
  8. Token captured at WS connect — STOMP auto-reconnect re-uses the same token variable; no refresh path.
  9. JWT signature is never verified client-side. No JWKS endpoint is referenced.

5.3 Trust boundary map (consolidated)

BoundarySource → SinkTrust gate observed in frontend code
Anonymous user → public blog HTMLcontent-module → BlogDetailPage dangerouslySetInnerHTMLNone — raw passthrough
Anonymous user → public pro landingcontent-module → JSON.stringify in <script type=application/ld+json>None — </script> not escaped
Backend → redirectUrl/billing/v1/subscription response → window.location.hrefTruthy check only
MercadoPago → tokenisation payloadapi.mercadopago.com/v1/card_tokensDirect browser POST; PAN in SPA DOM
Network → middleware XFFx-forwarded-for[0] → backend X-Forwarded-ForNo upstream proxy verification in code
WebSocket → UI stateSTOMP frame → WebSocketMessage.senderId / dataOnly JSON.parse + try/catch
WebSocket → navigationWebNotification.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:LineRuleSeverity / Confidence / ImpactClassification
1BlogDetailPage.tsx:101react-dangerouslysetinnerhtmlWARNING / MEDIUM / MEDIUMTRUE POSITIVE — raw block.html from backend; no defence-in-depth on the frontend
2pro/[categorySlug]/[slug]/page.tsx:194react-dangerouslysetinnerhtmlWARNING / MEDIUM / MEDIUMTRUE POSITIVE — JSON-LD payload, </script> not escaped; backend-supplied professional fields
3SafeHtml.tsx:36react-dangerouslysetinnerhtmlWARNING / MEDIUM / MEDIUMFALSE 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":

#LocationClassMitigation ownerSource
1mercado-pago-card-form.tsx:140-143 — window.location.href = redirectUrl without scheme/host validationOpen redirectFrontend (add allowlist) or backend (enforce MP host)§4.3
2add-card-form.tsx:66-79 — raw PAN/CVV → /billing/v1/payment-methodsPCI scope creepBackend (must tokenise, not store); frontend can switch to MP iframe§4.3
3utils/auth.ts:32-80 — Bearer token in non-HttpOnly cookie + localStorage fallback, 7d expiryXSS exfiltration targetTrade-off accepted; mitigations are CSP + reducing XSS sinks§4.1
4utils/auth.ts:44-73 — Safari fallback writes cookie with secure: falseCleartext token over HTTP if site ever served plaintextFrontend (probe with secure: true, sameSite: 'lax' before downgrading)§4.1
5auth-context.tsx:52-56 — JWT without exp claim is not rejectedIndefinite sessionFrontend (require exp); backend should always issue§4.1
6middleware.ts:65-82 — X-Forwarded-For injection without trusted-proxy allowlistIP spoofing into backend audit / rate-limitDeployment (trusted edge proxy)§4.1
7websocket-context.tsx:160-166 — STOMP captures token in closure; reconnect uses stale tokenStale auth on token rotationFrontend (re-read getAuthToken() in webSocketFactory)§4.4
8websocket-context.tsx:199-302 — client-built topics /topic/{userId} + UI trust in payload senderId/targetUserIdAuthz delegated 100% to brokerBackend (broker ACL on SUBSCRIBE)§4.4
9NotificationBell.tsx:150-156 — router.push(deepLinkUrl) from WS payloadInternal navigation tamperingBackend (constrain deepLinkUrl to same-origin paths)§4.4
10subscription-page.tsx:622-637 — MercadoPagoTest toggle reachable in prodTest endpoint exposureFrontend (env-flag) and backend (reject test payloads in prod)§4.3
11next.config.js — no CSP, only X-Frame-Options: SAMEORIGINNo defence-in-depthFrontend (add CSP header config)§4.3, §5
12auth.ts:82-98 — removeAuthToken omits domain when setter used domain: hostnamePossible cookie persistence after logoutFrontend (mirror set/remove flags)§4.1

6.3 Out-of-scope but flagged for follow-up

  • 48 localStorage / sessionStorage mentions in src/** 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

DependencyPinned / installed / latestRiskNote
next14.1.0 / 14.1.0 / 16.2.6CVEs, 2-year-old majorPinned 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 npmSingle maintainer, unmaintained on npm, CVEsSheetJS 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.0Version drift, CVEsWithin semver range; just refresh the lockfile.
sharp^0.33.5 / 0.33.x / 0.34.5Single maintainer (well-known), FFI to libvipsBump to ^0.34.x.
sockjs-client^1.6.1Single maintainer, unmaintained (last publish Oct 2022)Use native WebSocket via @stomp/stompjs brokerURL.
@types/socket.io-client^1.4.36Deprecated (2022)Remove.
socket.io-client^4.8.1Dead dep (zero imports)Remove.
@mercadopago/sdk-js^0.0.3Pre-1.0 version, dead depRemove or actually adopt for iframe-isolated tokenisation.
@mercadopago/sdk-react^1.0.7Dead depRemove 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:

  1. CMS sanitisation for blog ****block.html — does content-module accept arbitrary HTML on write, or constrain via a WYSIWYG? Who can author (admin / professional / open registration)?
  2. STOMP broker ACL — does the broker reject SUBSCRIBE /topic/{otherUserId} from a session authenticated as userId?
  3. MP initPoint origin enforcement — does billing-module copy MercadoPago's initPoint verbatim, or transform it? Could a backend bug substitute an attacker-controlled URL?
  4. exp**** claim guarantee — does the backend always issue JWTs with exp?
  5. Trusted edge proxy — does the deployment topology guarantee that x-forwarded-for reaching Next.js has been stripped/rewritten by a trusted upstream?
  6. AddCardForm**** backend behaviour — does /billing/v1/payment-methods tokenise and discard, or store raw PAN/CVV? If the latter, PCI scope is materially different.
  7. /api/kb/***** routing — no Next.js rewrite is configured (next.config.js:133-156); does nginx proxy this in prod?
  8. MP CheckoutPro return URLs — back_url / success_url / failure_url not configured in the frontend. Where does backend configure them?

9. Prioritized action plan

PrioActionFile / scopeEffort
P0Bump next to ^14.2.25 minimum (CVE-2025-29927)main-front/package.json~30 min + smoke test
P0Resolve 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
P1Sanitise block.html in BlogDetailPage (DOMPurify) or verify+document backend sanitisation in CMSBlogDetailPage.tsx:99-1032 h
P1Escape </script> and <!-- in JSON-LD payloadpro/[categorySlug]/[slug]/page.tsx:19415 min
P1Add host allowlist for redirectUrl before window.location.href (new URL(redirectUrl).host ∈ MP hosts)mercado-pago-card-form.tsx:140-1431 h
P1Gate MercadoPagoTest toggle behind process.env.NEXT_PUBLIC_ENABLE_MP_TEST (or remove the test component)subscription-page.tsx:622-63730 min
P1Migrate off xlsx@0.18.5 (CVEs unpatched on npm) — exceljs or SheetJS CDN tarballmain-front/src/utils/__tests__/ callers; export features4–8 h
P1Bump axios to 1.16.0 within ^1.9.0 rangemain-front/package-lock.json5 min
P2Add 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.js2–4 h (testing)
P2Strict role check in middleware — currently only verifies presence; should verify role from a server-trusted sourcemiddleware.ts:94-1062 h
P2Require JWT exp claim in validateAndDecodeToken (reject tokens without exp)auth-context.tsx:46-6915 min
P2WS token refresh — re-read getAuthToken() in webSocketFactory so STOMP reconnects use a fresh tokenwebsocket-context.tsx:160-1661 h
P2Remove dead deps: @mercadopago/sdk-js, @mercadopago/sdk-react, socket.io-client, @types/socket.io-clientmain-front/package.json30 min + npx depcheck validation
P2Remove unused CDN sockjs.min.js script from layout.tsxlayout.tsx:865 min
P2Fix cookie domain mismatch on removal — mirror set flags in Cookies.remove(..., { path, domain })utils/auth.ts:82-9830 min
P3Plan sockjs-client removal — native WebSocket via @stomp/stompjs brokerURLwebsocket-context.tsx:161-1632 h
P3Runtime validation (zod) on WS payloads (WebSocketMessage, ReadNotification, WebNotification, MessageResponse)websocket-context.tsx and consumers4 h
P3DRY the 5 fetchApi copies into one helper parameterised by service prefixlib/api.ts2 h
P3X-Forwarded-For trusted-proxy allowlist (requires deployment coordination)middleware.ts:65-82ops-dependent
P3Setup npm audit in GitLab CI to catch future regressions.gitlab-ci.yml30 min
P3Inventory 48 localStorage / sessionStorage call sites; document what's storedmain-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-building skill
  • Trail of Bits supply-chain-risk-auditor skill
  • Trail of Bits semgrep skill
  • 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.

MyMaestro

footer.tagline

footer.quickLinks

  • footer.home
  • footer.findJob
  • footer.findProfessional
  • footer.aboutUs

footer.categories

  • loading

footer.contactUs

  • support@mymaestro.cl
  • Chile

© 2026 MyMaestro. footer.allRightsReserved.

footer.termsOfServicefooter.privacyPolicyfooter.cookiePolicy