Skip to content

Someli Platform — Security Findings (Self-Contained)

Compiled 2026-05-22. This document stands alone — it does not link to any other document. Everything you need to understand and fix each issue is here: a plain-language glossary of the vulnerability types, the findings grouped by repository, and a ticket table (Appendix A) you can paste into a ticketing system.

Written for a developer who is new to security. Each finding tells you what it is, where it is, why it matters, and how to fix it. Recurring vulnerability types are explained once in the Glossary (§3) and referenced by a code like [G1], so the per-repo entries stay short.

Secret values are truncated (e.g. xoxb-3144…) so this document can be shared safely. The real values are in the source code at the cited file:line. Any secret named here must be treated as compromised and rotated — it has been committed to git.


1. How to use this document

  • Fixing issues top-down? Start with the 🔴 Critical findings, then 🟠 High. The repo sections (§4) are ordered by severity within each repo.
  • Creating tickets? Go to Appendix A — one row per finding, with a stable ticket ID (e.g. SEC-API-01), severity, category, location, and a one-line fix. The matching detail is in §4 under the same ID.
  • New to a vulnerability type? Read its glossary entry in §3 (the [G#] codes).
  • Severities follow §2. A "Phase" suggests urgency: 0a = this week, 0 = this quarter, 1 = 3–9 months, 2 = 9+ months.

Contents


2. Severity legend

Badge Meaning
🔴 Critical Exploitable now, leads directly to account/data compromise. Fix immediately (Phase 0a).
🟠 High Serious; leads to compromise with a small extra step, or exposes a live secret. Fix this quarter (Phase 0).
🟡 Medium Real weakness, harder to exploit or limited blast radius. Fix in 0–9 months.
Low Hardening / hygiene; low likelihood or low impact. Fix when convenient.

3. Vulnerability glossary

Each entry: what it is, why it matters, the general fix. The per-repo findings point here by code.

G1 — SQL injection

What: Building a database query by gluing user input straight into the SQL text, e.g. `... WHERE email='${req.body.email}'`. A value like ' OR '1'='1 changes what the query does. Why it matters: An attacker can read or change any data (dump password hashes, log in as anyone, delete rows). On a login endpoint this needs no account at all. General fix: Never concatenate input into SQL. Use parameterised queries — placeholders with a values array: con.query('... WHERE email = ?', [email]). The database driver then treats the input as data, never as SQL. Table/column names can't be parameterised — validate those against a fixed allow-list.

G2 — Hardcoded / committed secrets

What: API keys, tokens, passwords, or .env/credential files written into source code or baked into a Docker image. Why it matters: Everyone with repo (or image) access — current/former staff, contractors, anyone a clone leaked to — has the secret. Git keeps it in history forever, even after deletion. General fix: (1) Rotate the secret at its provider (assume it's already stolen). (2) Move it to an environment variable read via conf.js/process.env. (3) Remove the file from the image build and .gitignore it. (4) Scrub git history (git filter-repo/BFG) and force-push. (5) Add a secret scanner (gitleaks) to CI.

G3 — Insecure credential handling

What: Storing passwords in plaintext, comparing them with ==, or hashing them in the browser instead of the server. Why it matters: Plaintext passwords leak wholesale if the DB is read. A password == storedValue check can be bypassed if you can submit the stored value. Client-side hashing is security theatre — the hash becomes the de-facto password. General fix: Hash with bcrypt on the server at registration; verify with bcrypt.compare(). Never store or compare plaintext. Send the plaintext password over HTTPS (TLS is the transport protection) and hash it server-side.

G4 — Broken access control

What: Deciding what a user is allowed to do in the browser (hiding buttons by role) or only in a SQL WHERE clause, without the server independently checking the caller's identity/role on every request. Also covers login logic that grants access it shouldn't. Why it matters: A user can edit a cookie, call the API directly, or type a URL to do things they shouldn't. The UI is not a security boundary. General fix: Enforce authorisation on the server, per request, from the authenticated identity. Treat any role/account value sent by the client as untrusted. UI gating is UX only.

G5 — Insecure session-token storage

What: Keeping the login token where browser JavaScript can read it — localStorage or a cookie without the HttpOnly flag. Why it matters: If any script on the page is malicious or compromised (see G10), it can read the token and impersonate the user. JS-readable token + XSS = account takeover. General fix: Have the backend set the token as an HttpOnly; Secure; SameSite cookie so JavaScript can never read it; send requests with credentials included. Stop returning the token in the JSON body for the SPA to store.

G6 — Weak token lifecycle

What: Tokens that never expire, can't be reliably revoked (revocation list kept only in process memory), or are guessable/forgeable (predictable format). Why it matters: A stolen token works forever; a "logout" or "revoke" doesn't actually stop it; a predictable token can be forged without stealing anything. General fix: Put an expiry (exp) in tokens; store revocations in a shared, persistent place (DB/Redis), not memory; make tokens unguessable (signed/encrypted random values), never id_timestamp_id or Bearer <known value>.

G7 — Missing HTTP security headers (incl. clickjacking)

What: The server doesn't send protective response headers: Strict-Transport-Security (force HTTPS), X-Content-Type-Options: nosniff, X-Frame-Options/CSP frame-ancestors (stop your page being embedded in a hostile <iframe> — "clickjacking"), Referrer-Policy. Why it matters: Browsers can't enforce protections they're not told about. No frame-ancestors/X-Frame-Options means an attacker can frame your app and trick users into clicks. General fix: Add helmet() in Express apps; add the headers in nginx for SPAs: add_header X-Frame-Options "DENY", Content-Security-Policy "frame-ancestors 'none'", Strict-Transport-Security, X-Content-Type-Options nosniff, Referrer-Policy.

G8 — No Content Security Policy (CSP)

What: No CSP header telling the browser which script/style/connect sources are allowed. Why it matters: If an attacker injects a script (XSS, G10), nothing limits what it can do. CSP is the safety net that stops injected/remote scripts from running. General fix: Add a Content-Security-Policy header (nginx or meta tag) listing allowed sources; roll out in -Report-Only mode first, then enforce. Aim to remove 'unsafe-inline' over time.

G9 — No Subresource Integrity (SRI) / risky third-party scripts

What: Loading JavaScript from a CDN or third party with no integrity="sha384-…" hash, so the browser runs whatever the CDN serves. Why it matters: If the CDN (or that third party) is compromised, attacker JavaScript runs on your site with full access — including reading tokens (G5). General fix: Add integrity + crossorigin attributes to every external <script>/<link> (generate with openssl dgst -sha384 -binary | openssl base64 -A); better, self-host the file from npm so there's no third party to trust; remove scripts you don't need.

G10 — Cross-site scripting (XSS)

What: Rendering untrusted text as HTML — Vue's v-html, React's dangerouslySetInnerHTML, or element.innerHTML — so a <script> or <img onerror=…> in the data executes. Why it matters: Injected scripts run as the user: steal tokens (G5), perform actions, deface the page. AI-generated or uploaded content is a common source. General fix: Prefer plain text interpolation ({{ value }}). When HTML is genuinely needed, sanitise with DOMPurify first. Add a lint rule (vue/no-v-html, react/no-danger) to catch new uses.

G11 — CORS misconfiguration

What: Access-Control-Allow-Origin: * (e.g. app.use(cors()) with no options) — any website can call your API from a browser. Why it matters: Widens the attack surface; combined with cookie auth it enables cross-site request abuse. A wildcard origin is also incompatible with sending credentials. General fix: Configure an allow-list of known origins; set credentials: true only with a specific origin, never *.

G12 — No rate limiting

What: No cap on how many requests an IP/user can make, especially to /login / /authenticate. Why it matters: Attackers can brute-force passwords or "credential-stuff" (try leaked password lists) as fast as the server answers; also enables cost-driving abuse of AI endpoints. General fix: Add express-rate-limit — a tight limit on auth endpoints (e.g. 10/15 min per IP) and a looser global one. Behind nginx, set trust proxy so the real client IP is used.

G13 — Oversized request body limit (DoS)

What: Accepting very large request bodies (e.g. 50–150 MB JSON) on every route. Why it matters: One attacker sending big bodies can exhaust server memory (denial of service), especially on unauthenticated endpoints. General fix: Set a small global limit (e.g. 1mb); raise it per route only where large payloads are genuinely needed. Route binary uploads through a file-upload middleware with explicit size caps.

G14 — Sensitive data / PII in logs or browser storage

What: Writing passwords, tokens, hashes, or personal data (email, name) to server logs (console.log(req.body)) or localStorage. Why it matters: Logs and shared-browser storage are read by people and systems who shouldn't see credentials/PII; it's also a compliance problem. General fix: Never log credentials or whole request bodies; redact. Don't persist PII in localStorage; source it from app state or a /me endpoint and clear it on logout.

G15 — Outdated / EOL dependencies

What: Frameworks/libraries past end-of-life (e.g. Vue 2, Nuxt 2, Webpack 4) that no longer receive security patches; no automated update tooling. Why it matters: New vulnerabilities in these will never be fixed; transitive CVEs pile up unnoticed. General fix: Enable Dependabot (or Renovate) security PRs; run npm/yarn audit in CI; plan migration off EOL frameworks.

G16 — TLS / transport misconfiguration

What: Allowing deprecated TLS versions (TLS 1.0/1.1), leaking the server version banner, or serving mixed (http + https) content. Why it matters: Old TLS enables downgrade/interception attacks and fails PCI-DSS (relevant where payments are processed); version banners aid attackers; mixed content can be tampered with. General fix: ssl_protocols TLSv1.2 TLSv1.3; only; server_tokens off;; force HTTPS and use upgrade-insecure-requests.


4. Findings by repository

Seven repositories. Backends are Express + MySQL; the customer and designer web apps are Nuxt 2 / Vue 2; the admin web app is Vite + React. Many issues repeat across repos — that's expected (three backends were forked from one another) and each needs its own fix/ticket.


4.1 someli-api

Main customer-facing backend (Express + MySQL). The platform's largest and most security-critical service — it owns login.

🔴 SEC-API-01 — Login bypass: social:true logs in as anyone by email · Critical · [G4]

Where: routes/routes.js:6522-6620 (the /webauthenticate handler). Why it matters: When a request includes social: true, the handler (a) matches the account by email alone and (b) issues a valid session token even after the password check fails. The only gate is a shared constant password marker. An attacker who knows a victim's email can log in as them — no password. This is the most serious issue on the platform. Fix (Phase 0a): 1. Delete the branch that issues a token when social == true after the password comparison failed (around routes/routes.js:6612). 2. Social login must complete through the OAuth/Passport callback that sets the session — not via a flag on the password endpoint. 3. Remove the password == storedValue fallback (see SEC-API-03) so only a successful bcrypt.compare grants access.

🔴 SEC-API-02 — SQL injection in the login endpoints · Critical · [G1]

Where: /webauthenticate routes/routes.js:6529-6548; /authenticate routes/routes.js:6393-6403. (Hundreds of other interpolated queries exist platform-wide; the login ones are pre-auth and worst.) Why it matters: email/sid are glued straight into the SQL. An unauthenticated attacker can inject — dump password hashes via UNION, or bypass the login logic for any account. Fix (Phase 0a): 1. Make the query wrapper parameter-aware: at actions/actions.js:187 forward an optional values array — db.query(reqObj.q, reqObj.params || [], cb). 2. Rewrite the login queries with ? placeholders: WHERE m.username = ? / m.sid = ?, passing reqObj.params = [email, sid]. 3. Convert the direct con.query(\… id=${…}`)calls in the same handler (:6554,:6567,:6594,:6604`) to placeholder form.

🟠 SEC-API-03 — Plaintext password storage & comparison · High · [G3]

Where: /register (stores plaintext); /login routes/routes.js:3039 and /webauthenticate :6603 (password == storedValue comparison — "pass-the-hash"). Why it matters: Passwords readable if the DB leaks; the == comparison can be satisfied by submitting the stored value (which SEC-API-02 can leak). Fix (Phase 0): 1. Hash passwords with bcrypt at registration. 2. Verify only with bcrypt.compare(); delete every password == row.password branch. 3. Select the account by sid/email with a placeholder; never put the password in the SQL.

🟠 SEC-API-04 — Forgeable tokens · High · [G6]

Where: /auth/login mints a plaintext token id_timestamp_id (routes/routes.js:3060); on success the server stores auth_token = 'Bearer ' + sid where sid is non-secret (:6594). Why it matters: Anyone who sees the token format can forge one / extract the user ID; a non-secret stored token is guessable. Fix (Phase 0): 1. Replace the plaintext token at :3060 with the existing AES helper: encryptData({ userId, defaultPaymentId }) (already imported at :104). 2. Stop persisting Bearer <sid>; use an unguessable signed/encrypted token with an expiry.

🟠 SEC-API-05 — Hardcoded secrets (Slack token, Polotno key, session secret) · High · [G2]

Where: Slack token xoxb-3144… in 19 job files (e.g. job_color_check.js:11); Polotno key FXZv… in 49 files (e.g. job_design_generation.js:119); express-session secret "3eB(2…" in server.js:13. Why it matters: Anyone with repo access can use these; rotating today means editing dozens of files. Fix (Phase 0a): 1. Rotate all three at their providers. 2. Add SLACK_BOT_TOKEN, POLOTNO_KEY, SESSION_SECRET to .env + conf.js. 3. Replace the literals (a single shared accessor for Slack/Polotno makes future rotation one change); generate the session secret with crypto.randomBytes(32).toString('hex'). 4. Scrub git history; add a secret scanner to CI.

🟠 SEC-API-06 — No rate limiting on auth endpoints · High · [G12]

Where: server.js / routes/routes.js — no limiter on /login (:3024), /authenticate (:6387), /webauthenticate (:6522), /register (:2421). Why it matters: Brute force / credential stuffing is unthrottled; AI endpoints can be hammered to drive cost. Fix (Phase 0): 1. yarn add express-rate-limit. 2. Add a tight authLimiter (e.g. 10 per 15 min/IP) on each credential route; a looser global one elsewhere. 3. Set app.set('trust proxy', 1) (nginx fronts the app) so limiting keys on the real IP.

🟡 SEC-API-07 — Credentials logged at login · Medium · [G14]

Where: console.log("resss", req.body) routes/routes.js:6525; console.log("ress", …, result[0].password, reqObj.password) :6585. Why it matters: Cleartext passwords and stored hashes land in production logs. Fix (Phase 0): Remove these logs (and any that print req.body/passwords/hashes); redact if a log is needed.

🟡 SEC-API-08 — CORS accepts all origins · Medium · [G11]

Where: app.use(cors()) with no options, server.js:145. Why it matters: Any website's JS can call the API. Fix (Phase 0): Configure an origin allow-list from conf.ALLOWED_ORIGINS with credentials: true; mount it before routes.

🟡 SEC-API-09 — No security headers (helmet not loaded) · Medium · [G7]

Where: server.jshelmet is in package.json:54 but never wired in. Why it matters: None of the protective headers are sent. Fix (Phase 0): Add app.use(helmet({ contentSecurityPolicy: false })) as the first middleware (it's a JSON API). Ensure nginx isn't setting duplicates.

🟡 SEC-API-10 — 150 MB request body limit · Medium · [G13]

Where: express.json({ limit: '150mb', parameterLimit: 50000 }) server.js:24. Why it matters: Memory-exhaustion DoS, including on unauthenticated routes. Fix (Phase 0): Lower global to 1mb/parameterLimit: 1000; per-route override only where large payloads are needed; route binary uploads through express-fileupload.

🟡 SEC-API-11 — Input validation on one route only · Medium · [G1]

Where: middlewares/validation.js defines only registerVal. Why it matters: Most handlers trust input; no boundary validation supports the injection risks above. Fix (Phase 0): Add validator chains (e.g. loginVal with isEmail()/notEmpty()) and apply them on each route before the handler.

🟡 SEC-API-12 — Token revocation kept in memory · Medium · [G6]

Where: helper/revokeToken.js — revoked tokens in an in-process Set. Why it matters: Restart re-validates "revoked" tokens; revocation doesn't apply across replicas; tokens have no expiry, so a stolen one is effectively permanent. Fix (Phase 0): Persist revocations in MySQL/Redis (shared, survives restarts); pair with token expiry.

⚪ SEC-API-13 — Middleware order / no error handler · Low · [G7]

Where: server.js — CORS mounted after session (:145); no global error handler; no request logger. Why it matters: Unconventional ordering; uncaught errors may leak stack traces; requests aren't traceable. Fix (Phase 1): Order helmet → cors → session → fileUpload → body parser; add a final error-handling middleware that hides stack traces in production; add a request logger (morgan).


4.2 someli-platform

Customer-facing web app (Nuxt 2 / Vue 2 single-page app), served behind nginx.

🔴 SEC-WEB-01 — AWS access key + secret hardcoded · Critical · [G2]

Where: plugins/aws_sdk.js:4-6 (accessKeyId/secretAccessKey), committed since 2022-08-01. Why it matters: Live AWS credentials in git history for ~3.5 years — anyone with any past repo access has them. Treat as a breach. Fix (Phase 0a): 1. Rotate the AWS key now in IAM; audit CloudTrail for misuse since 2022-08-01. 2. Delete the file; scrub git history (git filter-repo/BFG), force-push, re-clone. 3. Add gitleaks to CI.

Where: token and usedetail cookies set without HttpOnly (auth strategy in nuxt.config.js:285-298). Why it matters: Any same-origin script (including the CDN scripts and v-html sinks below) can read the token → account takeover. Fix (Phase 0): Backend sets an HttpOnly; Secure; SameSite cookie; switch the @nuxtjs/auth strategy to server-set-cookie mode and stop writing the token from JS; send requests with withCredentials.

🟠 SEC-WEB-03 — No Content Security Policy · High · [G8]

Where: No CSP in nginx.conf or app; nuxt.config.js:121 sets __dangerouslyDisableSanitizers: ['script']. Why it matters: Any XSS has unrestricted script execution. Fix (Phase 0): Add a baseline CSP in nginx (allow self + the known CDN hosts + frame-ancestors 'none'); tighten over time; move inline analytics to files to drop 'unsafe-inline'.

🟠 SEC-WEB-04 — No Subresource Integrity on 14 CDN scripts · High · [G9]

Where: nuxt.config.js:23-119 (head.script) — includes flagmeister.github.io (a GitHub-Pages-hosted third party). Why it matters: A compromised CDN runs arbitrary JS with access to the token. Fix (Phase 0a): Add integrity="sha384-…" + crossorigin to each pinned script; self-host where possible.

🟠 SEC-WEB-05 — v-html renders unsanitised HTML (incl. AI chat output) · High · [G10]

Where: 9 v-html sites across 4 components; worst is components/Chatbot.vue:1421-1453 (renders AI/server text as HTML). Why it matters: Stored/reflected XSS — markup in the chat/profile data executes. Fix (Phase 0): Add DOMPurify; wrap each v-html value in a safeHtml() sanitiser or switch to {{ }}; add the vue/no-v-html lint rule.

🟠 SEC-WEB-06 — End-of-life framework stack · High · [G15]

Where: vue@2.7, nuxt@2.18, webpack@4 — all EOL. Why it matters: No security patches for new vulnerabilities. Fix (Phase 2): Plan Vue 2→3 / Nuxt 2→3 migration; until then treat dependency PRs as the only patch channel.

🟠 SEC-WEB-07 — No automated dependency updates · High · [G15]

Where: No .github/dependabot.yml / Renovate. Why it matters: Transitive CVEs accumulate unnoticed. Fix (Phase 0): Add Dependabot security PRs (cover the /polotno-editor sub-package too); run yarn audit in CI.

Where: middleware/head.js branches on roleType/account_role read from cookies the user can edit. Why it matters: A user can change their cookie to escalate privileges; the UI is the only gate. Fix (Phase 1): Enforce roles on the backend per request; fetch the user record from an authenticated /me endpoint rather than trusting cookie values.

🟡 SEC-WEB-09 — Missing nginx security headers · Medium · [G7]

Where: nginx.conf sets no HSTS / X-Frame-Options / X-Content-Type-Options / Referrer-Policy. Why it matters: Browsers can't enforce protections, including anti-framing (clickjacking). Fix (Phase 0): Add the header block (HSTS, X-Content-Type-Options nosniff, Referrer-Policy, plus the anti-framing headers in SEC-WEB-10).

🟡 SEC-WEB-10 — Clickjacking: app is framable (external report) · Medium · [G7]

Where: No X-Frame-Options/CSP frame-ancestors anywhere. (An external researcher reported this 2026-05-22; their wider claims were unsubstantiated, but the framing gap is real.) Why it matters: A hostile site can embed the app in an invisible <iframe> and trick a logged-in user into clicking a one-click action. Fix (Phase 0): add_header X-Frame-Options "DENY" always; and add_header Content-Security-Policy "frame-ancestors 'none'" always;. If any single-click destructive action exists on a framable page, add a confirm/re-auth step too.

🟡 SEC-WEB-11 — Deprecated TLS 1.0/1.1 enabled · Medium · [G16]

Where: nginx.conf:36 ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;. Why it matters: TLS 1.0/1.1 are deprecated and PCI-DSS-prohibited (the app takes payments). Fix (Phase 0): ssl_protocols TLSv1.2 TLSv1.3;.

🟡 SEC-WEB-13 — CORS allow-list unverified · Medium · [G11]

Where: Frontend uses SameSite=None cookies; backend CORS config not confirmed. Why it matters: If the backend allows * with credentials, cross-site abuse is possible. Fix (Phase 0): Confirm the backend CORS allow-list is a specific origin, not * (see SEC-API-08).

🟡 SEC-WEB-18 — Ad-blockers break payment SDKs · Medium (operational) · [—]

Where: Stripe.js / Paddle.js loaded globally via nuxt.config.js head.script. Why it matters: Common blockers stop these URLs → silent checkout failures (revenue loss), not a security hole. Fix (Phase 0): Load payment SDKs only on checkout routes; detect a failed load and show a clear message; instrument it.

⚪ SEC-WEB-12 — nginx version disclosure · Low · [G7]

Where: nginx.conf:26server_tokens off; is commented out. Fix (Phase 0): Uncomment server_tokens off;.

⚪ SEC-WEB-14 — Public env vars in bundle (verify no secrets) · Low · [G2]

Where: Build-time .env values ship in the client bundle. Fix (Phase 0a): Grep a fresh build to confirm no OAuth client secrets / EXPRESS_SECRET are bundled; move any that are server-side.

⚪ SEC-WEB-15 — Token possibly in URL (verify) · Low · [G6]

Where: Auth flows — spot-check whether any put the token in query params. Fix (Phase 1): Confirm; if found, move to header/cookie (URLs leak via history/logs/Referer).

⚪ SEC-WEB-16 — Hardcoded telemetry/license keys · Low · [G2]

Where: Polotno license key + analytics IDs in nuxt.config.js/polotno-editor/. Fix (Phase 1): Move to env where applicable; confirm Polotno license terms tolerate a committed key.

⚪ SEC-WEB-17 — Mixed content · Low · [G16]

Where: Spot-check pages//components/ for http:// asset URLs. Fix (Phase 1): Use HTTPS everywhere; add upgrade-insecure-requests to the CSP.


4.3 designer-api

Backend for the internal designer tool (Express + MySQL). Thinner than someli-api; no shared auth middleware.

🟠 SEC-DAPI-01 — Hardcoded Slack bot token · High · [G2]

Where: teamsnotification.js:1-2 (xoxb-3144…, channel C05H25MDY3Z). Fix (Phase 0a): Rotate the token; move to SLACK_BOT_TOKEN/SLACK_CHANNEL_ID in conf.js; replace the literals; scrub history.

🟠 SEC-DAPI-02 — Hardcoded Unsplash access key · High · [G2]

Where: routes/routes.js:47-50 (accessKey: 't0uI…'). Fix (Phase 0a): Rotate at Unsplash; move to conf.UNSPLASH_ACCESS_KEY; fail fast if unset.

🟠 SEC-DAPI-03 — No shared auth middleware · High · [G4]

Where: No middlewares/ directory; each handler does its own auth (or forgets). Why it matters: A new endpoint added by someone who forgets the check is publicly callable. Fix (Phase 0): Create middlewares/auth.js validating the Bearer <sid> token against tMember (parameterised query); mount it on the router with an explicit public allow-list (/webauthenticate, health); add the folder to the Dockerfile COPY.

🟠 SEC-DAPI-06 — Committed Google OAuth client secret · High · [G2]

Where: conf/credentials.json (tracked, not gitignored) — contains client_secret. Fix (Phase 0a): Rotate in Google Cloud Console; git rm --cached + .gitignore; inject at runtime; scrub history. (Also remove from the Docker image — see SEC-DAPI-13.)

🟠 SEC-DAPI-13 — .env baked into the Docker image · High · [G2]

Where: Dockerfile:35 COPY .env conf.js favicon.ico ./. Why it matters: Every secret (OPENAI_API_KEY, AWS keys, DB password, SendGrid/Pexels/Pixabay, etc.) is readable by anyone who can pull the image. Fix (Phase 0a): Drop .env from the COPY; inject secrets at runtime (--env-file, secrets manager, or orchestrator secrets); add .env to .dockerignore; rotate all baked-in secrets.

🟡 SEC-DAPI-04 — CORS wildcard + malformed credentials header · Medium · [G11]

Where: server.js:8-15cors() + a manual res.header('"Access-Control-Allow-Credentials" : true') (the whole string is the header name). Fix (Phase 0): Replace with one configured cors({ origin: allowlist, credentials: true }); delete the malformed manual header.

🟡 SEC-DAPI-07 — SQL injection in interpolated queries · Medium · [G1]

Where: request-controlled values interpolated at routes/routes.js:1499, :1577, :1580, :1620, :1700, :1703, :1807, :1810, :1845, and the login query :187. Fix (Phase 0): Parameterise these with ? placeholders + values array; extend the DB wrapper (modules/dbDriver/lib/mysql.js) to forward bind params; allow-list any dynamic table names.

🟡 SEC-DAPI-05 — 50 MB body limit · Medium · [G13]

Where: server.js:17 global bodyparser.json({ limit: '50MB' }). Fix (Phase 1): Lower global to 256kb; per-route override for Polotno/image endpoints; cap express-fileupload.

🟡 SEC-DAPI-10 — No rate limiting · Medium · [G12]

Where: No express-rate-limit; /webauthenticate (routes/routes.js:171) is unauthenticated. Fix (Phase 0): Add express-rate-limit — tight on /webauthenticate, looser globally; trust proxy for the real IP.

🟡 SEC-DAPI-08 — forEach over async work (correctness) · Medium · [—]

Where: 18 job files use forEach(async …) (e.g. job_get_image.js:304). Why it matters: forEach doesn't await — race conditions, unbounded concurrency on OpenAI/S3, re-entrancy bugs. (Reliability, not a direct vuln.) Fix (Phase 0): Use for…of with await, or p-limit for bounded parallelism; reset re-entrancy guards in a finally.

⚪ SEC-DAPI-09 — Polotno license key hardcoded · Low · [G2]

Where: routes/routes.jskey: 'FXZv…' in 11 places (+ job scripts). Fix (Phase 0a): Move to conf.POLOTNO_KEY; rotate if your plan treats the key as secret (some Polotno keys are domain-scoped/public).

⚪ SEC-DAPI-11 — No app-layer security headers · Low · [G7]

Where: server.js sets no helmet/HSTS. Fix (Phase 1): Add helmet({ hsts: … }); confirm nginx redirects HTTP→HTTPS.

⚪ SEC-DAPI-12 — nginx.conf not audited · Low · [G7]

Where: Committed nginx.conf shipped via Dockerfile:36, contents unreviewed. Fix (Phase 1): Review it; ensure server_tokens off, HSTS, X-Content-Type-Options, matching client_max_body_size; nginx -t before deploy.


4.4 Someli-admin-api

Backend for the internal admin console (Express + MySQL). Forked from someli-api.

🟠 SEC-AAPI-01 — Hardcoded session secret · High · [G2]

Where: server.js:11 secret: "3eB(2…" (same for dev/uat/prod); saveUninitialized: true. Fix (Phase 0a): Rotate (random 48 bytes, per-env); move to conf.SESSION_SECRET with fail-fast; set saveUninitialized: false + httpOnly/secure/sameSite cookie; scrub history.

🟠 SEC-AAPI-02 — Hardcoded Slack bot token · High · [G2]

Where: routes/auth.js:20-21 (xoxb-3144…, channel C05TS9AHBH6). Fix (Phase 0a): Rotate; move to conf; replace literals at :20-21,:25; reduce bot scopes to chat:write; scrub history.

🟠 SEC-AAPI-10 — Committed Google OAuth client secret · High · [G2]

Where: conf/credentials.json (tracked, committed 75b92e9, not gitignored) — client_secret for an OAuth web client. Fix (Phase 0a): Rotate in Google Cloud Console; git rm --cached + .gitignore; inject at deploy; scrub history.

🟡 SEC-AAPI-03 — In-memory token revocation · Medium · [G6]

Where: helper/revokeToken.js — revoked tokens in a Set. Fix (Phase 0): Persist in a tRevokedToken table (parameterised insert/lookup); make middlewares/auth.js check it; add expiry cleanup.

🟡 SEC-AAPI-04 — Tokens never expire · Medium · [G6]

Where: helper/tokenGenerator.js:3,8JWT_EXPIRES_IN not in conf.js, so expiresIn is undefined → perpetual token. Fix (Phase 0): Set JWT_EXPIRES_IN (e.g. 24h) with a safe default in code; jwt.verify then rejects expired tokens automatically.

🟡 SEC-AAPI-05 — CORS wildcard · Medium · [G11]

Where: server.js:75 app.use(cors()). Fix (Phase 0): Allow-list from conf.CORS_ORIGINS (e.g. https://admin.someli.ai, http://localhost:8080) with credentials: true.

🟡 SEC-AAPI-07 — Role gating in SQL only · Medium · [G4]

Where: No requireRole middleware; each handler enforces role via WHERE filters (e.g. routes/auth.js:74,93-102role=all yields an empty filter = everything). Fix (Phase 0): Add a requireRole(allowed) middleware reading res.role; apply per route; make the role=all branch admin-only.

🟡 SEC-AAPI-08 — SQL injection via dynamic filters · Medium · [G1]

Where: request-derived interpolation, e.g. the member-update at routes/auth.js:410 (sid='${reqObj.email}' … WHERE id=${reqObj.id}) and member-list at routes/routes.js:817-821,837. Fix (Phase 0): Rewrite with ? placeholders + values array; allow-list dynamic identifiers; grep for the rest and classify.

⚪ SEC-AAPI-06 — Webhook body-parser exemption with no handlers · Low · [—]

Where: server.js:19-25 exempts Stripe/Paddle webhook paths from JSON parsing, but no handlers exist (they 404). Fix (Phase 1): If vestigial, delete the exemption; if planned, mount handlers that verify the signature over the raw body before trusting it.

⚪ SEC-AAPI-09 — 150 MB body limit · Low · [G13]

Where: server.js:23 express.json({ limit: '150mb', parameterLimit: 50000 }). Fix (Phase 1): Lower to 1mb; per-route override where needed; cap express-fileupload; add helmet.


4.5 someli-dashboard-be

Tiny standalone Express service mirroring someli-api/dashboard/. In production the in-process copy inside someli-api runs; this standalone is for local dev. The two have drifted — fixes here should also be applied to the copy inside someli-api/dashboard/.

🟡 SEC-DASH-02 — Auth-by-convention · Medium · [G4]

Where: Handlers read res.userId/res.accountId assuming an upstream middleware set them; if mounted without that upstream auth, they run unauthenticated. Fix (Phase 0): Add an in-router guard (routes/index.js:24) that 401s when res.userId/res.accountId are missing — fails closed.

⚪ SEC-DASH-01 — Fallback session secret "change-me" · Low · [G2]

Where: server.js:9 secret: process.env.SESSION_SECRET || "change-me". Fix: Remove the unused express-session entirely (nothing reads req.session), or fail-fast if SESSION_SECRET is unset + add httpOnly/secure/sameSite.

⚪ SEC-DASH-03 — Mock auth is deployable · Low · [G4]

Where: mock/middlewares/auth.js sets fake userId/accountId when NODE_ENV=development. Why it matters: If ever deployed with NODE_ENV=development, every request is a fake authenticated user. Fix: Gate the mock behind an explicit ALLOW_MOCK_AUTH=yes flag that throws otherwise; set NODE_ENV=production in prod.

⚪ SEC-DASH-04 — No standalone hardening · Low · [G7]

Where: server.js has no helmet/CORS allow-list/rate-limit when run on its own. Fix: Add helmet(), a CORS allow-list, and express-rate-limit on /dashboard/*.

⚪ SEC-DASH-05 — Spurious / deprecated dependencies · Low · [G15]

Where: package.json declares unused jsonwebtoken, crypto-js, request (deprecated), node-cron, path (npm clone of a built-in). Fix: Confirm unused, then npm uninstall them; run npm audit.


4.6 admin_console_R

Internal admin web app (Vite + React + TypeScript + shadcn/ui) — the only modern frontend. Bootstrapped from Lovable.dev.

🟡 SEC-ADMINUI-01 — Auth token in localStorage · Medium · [G5]

Where: src/context/AuthContext.tsx:32-34, src/services/api.ts:16-18. Why it matters: XSS-readable; combined with no server-side expiry/revocation → long-lived takeover. Fix (Phase 0): Move to an HttpOnly; Secure; SameSite=Strict cookie set by the backend (fetch(..., { credentials: "include" })); interim, use in-memory/sessionStorage not localStorage.

🟡 SEC-ADMINUI-03 — Third-party Lovable.dev script in production · Medium · [G9]

Where: index.html:14-15<script src="https://cdn.gpteng.co/gptengineer.js"> with no SRI. Why it matters: If that CDN is compromised, arbitrary JS runs in admin staff browsers. Fix (Phase 0): Remove it from production (it's only needed inside the Lovable editor) — delete the tag, or strip it in a Vite transformIndexHtml plugin when mode !== 'development'; if it must stay, pin with integrity + crossorigin.

🟡 SEC-ADMINUI-04 — No Content Security Policy · Medium · [G8]

Where: No CSP header or meta tag in the repo. Fix (Phase 0): Add a CSP at the nginx layer (or meta tag): default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' <api>; frame-ancestors 'none'; object-src 'none'; roll out report-only first.

🟡 SEC-ADMINUI-11 — No 401 → logout · Medium · [G6]

Where: src/services/api.ts:5-13 (handleResponse) doesn't branch on status. Why it matters: A revoked/expired token leaves the user "logged in" client-side with every action failing. Fix (Phase 0): On 401/403, clear auth state and redirect to /login (register a module-level callback that AuthContext wires to logout()).

⚪ SEC-ADMINUI-02 — Hardcoded Apptype header · Low · [G2]

Where: src/services/api.ts:23 btoa("admin-console") while ENV.APP_TYPE exists. Fix: Use btoa(ENV.APP_TYPE) (from src/config/env.ts:8); fail loudly if unset. (Not a secret — just dead config.)

⚪ SEC-ADMINUI-05 — No SRI · Low · [G9]

Where: The only external script is the Lovable one (SEC-ADMINUI-03). Fix: Resolve via SEC-ADMINUI-03; add a CI grep that fails on an https:// <script> without integrity.

⚪ SEC-ADMINUI-09 — dangerouslySetInnerHTML without sanitisation · Low · [G10]

Where: src/components/ui/chart.tsx:79 — shadcn ChartStyle, builds inline CSS from a developer-defined config (not user input today). Fix: Keep config developer-defined; if chart colours/labels ever come from API/user data, validate them before interpolation; add the react/no-danger lint rule; use DOMPurify for any real HTML rendering.

⚪ SEC-ADMINUI-10 — PII (user_email etc.) in localStorage · Low · [G14]

Where: user_email/useid/user_first_name written in AuthContext.tsx and read across pages; logout() (:206) only clears the token. Fix: Source these from auth context / a /me endpoint; if cached, use sessionStorage; clear all keys on logout.

⚪ SEC-ADMINUI-06 — HMR overlay disabled (dev-only) · Low · [—]

Where: vite.config.ts:18-21 hmr: { overlay: false }. Fix: Re-enable the overlay (dev quality-of-life; no production impact).

⚪ SEC-ADMINUI-07 — lovable-tagger dev plugin · Low · [G15]

Where: vite.config.ts:25 (gated to dev). Fix: Keep the mode === 'development' gate; verify dist/ has no data-lovable; remove entirely if Lovable is retired.

⚪ SEC-ADMINUI-08 — Minimal Vite fs.deny (dev-only) · Low · [—]

Where: vite.config.ts:14-17 deny: ['.env', '.git']. Fix: Broaden to ['.env', '.env.*', '*.pem', '*.key', '.git', '.git/**']; keep strict: true; don't expose the dev server beyond localhost.


4.7 Someli-Designer

Internal designer web app (Nuxt 2 / Vue 2 + embedded Polotno editor).

🟠 SEC-DESUI-01 — Auth guard is per-page, not global · High (verify) · [G4]

Where: nuxt.config.js:49-51 registers only axios middleware globally; middleware: 'auth' is declared on 53 pages individually (e.g. pages/AI-Content.vue:57). Why it matters: Any new page that forgets the line ships unguarded. Fix (Phase 0): Make the guard global — router: { middleware: ['auth', 'axios'] }; public pages opt out with auth: 'guest'; regression-test with a cleared session.

Where: @nuxtjs/auth local strategy default + the userdetail cookie (read in middleware/guest.js:3). Fix (Phase 0, needs backend coord): Backend sets an HttpOnly cookie; switch the strategy to cookie mode (token: { property: false }); hydrate the user from GET /me; drop the userdetail cookie.

🟡 SEC-DESUI-03 — No server-side logout · Medium · [G6]

Where: nuxt.config.js:72 logout: false — the FE never tells the backend to invalidate the token. Fix (Phase 0, needs backend coord): Add POST /logout (revoke token + clear cookie); set logout: { url: 'logout', method: 'post' }; wire a logout control in components/Navbar.vue.

🟡 SEC-DESUI-04 — Role gating UI-only · Medium · [G4]

Where: components/Navbar.vue hides items by role_type (magic numbers 1/2/6/12/13); URL-typing bypasses it. Fix (Phase 1): Enforce roles on the backend per route (authoritative); add an FE route guard as defence-in-depth; replace magic numbers with named constants.

🟡 SEC-DESUI-06 — No Content Security Policy · Medium · [G8]

Where: nginx.conf is referenced by Dockerfile:77 but not committed, so no CSP is reviewable. Fix (Phase 0): Commit nginx.conf; add a CSP whitelisting jsDelivr/Hotjar/Polotno/Pexels + the companion headers (HSTS, X-Frame-Options, X-Content-Type-Options); report-only first.

⚪ SEC-DESUI-05 — CDN scripts without SRI · Low · [G9]

Where: nuxt.config.js:16-20 — jsDelivr Bootstrap bundle, no integrity. Fix (Phase 1): Add integrity + crossorigin, or self-host from npm (Bootstrap 5.3.3 is already a dependency).

⚪ SEC-DESUI-08 — Hardcoded Polotno key + design data in localStorage · Low · [G2]

Where: polotno-editor/index.js:7 key: "FXZv…" (also in committed polotno-bundle.js); designs/brand data persisted to localStorage. Fix (Phase 1): Move the key to a build-time env var; rotate (confirm origin-binding/terms); scope persisted design keys per user and clear on logout; strip design-state console.logs in polotno-editor/App.js.

⚪ SEC-DESUI-09 — Client-side bcrypt hashing · Low · [G3]

Where: pages/login.vue:98,156-160 hash the password in the browser before login. Why it matters: A browser-side bcrypt with a random salt is non-deterministic and can't be verified server-side — the hash just becomes the password, providing no protection. Fix (Phase 1): Remove client-side hashing; send the plaintext password over HTTPS; verify with bcrypt on the backend; drop the bcryptjs FE dependency.

⚪ SEC-DESUI-10 — v-html renders AI/PDF content · Low · [G10]

Where: 7 pages (e.g. pages/AI-Content.vue:38, pages/pdfToContent.vue:36). Fix (Phase 1): Sanitise with a DOMPurify Vue filter (v-html="x | clean"), or use {{ }} where markup isn't needed.

⚪ SEC-DESUI-07 — Bootstrap-Vue v2 ↔ Bootstrap v5 mismatch (UX) · Low · [—]

Where: bootstrap-vue@2.x (targets BS4) + bootstrap@5.3.3 + a CDN BS5 bundle loaded together. Fix (Phase 1): Pick one Bootstrap line; remove the redundant CDN script (also closes SEC-DESUI-05).


Appendix A — All findings (ticket table)

One row per finding. Columns: ID (stable ticket key), Repo, Sev, Cat (glossary code), Title, Key location, Fix summary, Phase. Detail for each ID is in §4 under the same ID.

ID Repo Sev Cat Title Key location Fix summary Phase
SEC-API-01 someli-api 🔴 Critical G4 Login bypass via social:true routes/routes.js:6612 Delete the social token-grant branch; OAuth-only social login 0a
SEC-API-02 someli-api 🔴 Critical G1 SQL injection in login endpoints routes/routes.js:6529,6393 Parameterise email/sid; make queryAll param-aware 0a
SEC-API-03 someli-api 🟠 High G3 Plaintext password storage & compare routes/routes.js:3039,6603 bcrypt only; remove == comparisons 0
SEC-API-04 someli-api 🟠 High G6 Forgeable tokens routes/routes.js:3060,6594 AES token; drop Bearer+sid; add expiry 0
SEC-API-05 someli-api 🟠 High G2 Hardcoded Slack/Polotno/session secrets job_color_check.js:11 +67 files Rotate; move to env; replace literals; scrub history 0a
SEC-API-06 someli-api 🟠 High G12 No rate limiting on auth routes/routes.js:3024,6387,6522 express-rate-limit on auth routes 0
SEC-API-07 someli-api 🟡 Medium G14 Credentials logged at login routes/routes.js:6525,6585 Remove credential/req.body logs 0
SEC-API-08 someli-api 🟡 Medium G11 CORS accepts all origins server.js:145 Origin allow-list + credentials 0
SEC-API-09 someli-api 🟡 Medium G7 No security headers (helmet) server.js app.use(helmet(...)) 0
SEC-API-10 someli-api 🟡 Medium G13 150 MB body limit server.js:24 Global 1mb; per-route override 0
SEC-API-11 someli-api 🟡 Medium G1 Input validation on one route only middlewares/validation.js Validator chains per route 0
SEC-API-12 someli-api 🟡 Medium G6 In-memory token revocation helper/revokeToken.js Persist revocations; add expiry 0
SEC-API-13 someli-api ⚪ Low G7 Middleware order / no error handler server.js Reorder; add error handler + logger 1
SEC-WEB-01 someli-platform 🔴 Critical G2 Hardcoded AWS access key + secret plugins/aws_sdk.js:4-6 Rotate; delete; scrub history 0a
SEC-WEB-02 someli-platform 🟠 High G5 Token in browser-readable cookie nuxt.config.js:285-298 Server-set HttpOnly cookie 0
SEC-WEB-03 someli-platform 🟠 High G8 No Content Security Policy nginx.conf Add nginx CSP; drop unsafe-inline 0
SEC-WEB-04 someli-platform 🟠 High G9 No SRI on 14 CDN scripts nuxt.config.js:23-119 Add integrity; self-host 0a
SEC-WEB-05 someli-platform 🟠 High G10 v-html XSS (AI chat output) components/Chatbot.vue:1421 DOMPurify; vue/no-v-html 0
SEC-WEB-06 someli-platform 🟠 High G15 EOL Vue 2 / Nuxt 2 / Webpack 4 package.json Plan Vue3/Nuxt3 migration 2
SEC-WEB-07 someli-platform 🟠 High G15 No automated dependency updates .github/ Enable Dependabot; yarn audit CI 0
SEC-WEB-08 someli-platform 🟠 High G4 Role gating trusts editable cookie middleware/head.js Enforce roles server-side 1
SEC-WEB-09 someli-platform 🟡 Medium G7 Missing nginx security headers nginx.conf Add HSTS/XCTO/Referrer-Policy 0
SEC-WEB-10 someli-platform 🟡 Medium G7 Clickjacking — app is framable nginx.conf X-Frame-Options + frame-ancestors 0
SEC-WEB-11 someli-platform 🟡 Medium G16 TLS 1.0/1.1 enabled nginx.conf:36 ssl_protocols TLSv1.2 TLSv1.3 0
SEC-WEB-13 someli-platform 🟡 Medium G11 CORS allow-list unverified backend Confirm specific origin, not * 0
SEC-WEB-18 someli-platform 🟡 Medium Ad-blockers break payment SDKs nuxt.config.js Load SDKs on checkout; detect failure 0
SEC-WEB-12 someli-platform ⚪ Low G7 nginx version disclosure nginx.conf:26 server_tokens off 0
SEC-WEB-14 someli-platform ⚪ Low G2 Public env vars in bundle build Grep build; no secrets bundled 0a
SEC-WEB-15 someli-platform ⚪ Low G6 Token possibly in URL auth flows Verify; move to header/cookie 1
SEC-WEB-16 someli-platform ⚪ Low G2 Hardcoded telemetry/license keys nuxt.config.js Move to env; confirm Polotno terms 1
SEC-WEB-17 someli-platform ⚪ Low G16 Mixed content pages/,components/ HTTPS only; upgrade-insecure-requests 1
SEC-DAPI-01 designer-api 🟠 High G2 Hardcoded Slack token teamsnotification.js:1 Rotate; move to env; scrub history 0a
SEC-DAPI-02 designer-api 🟠 High G2 Hardcoded Unsplash key routes/routes.js:47-50 Rotate; move to env 0a
SEC-DAPI-03 designer-api 🟠 High G4 No shared auth middleware (no middlewares/) Add auth middleware + public allow-list 0
SEC-DAPI-06 designer-api 🟠 High G2 Committed Google OAuth secret conf/credentials.json Rotate; untrack; inject at runtime 0a
SEC-DAPI-13 designer-api 🟠 High G2 .env baked into Docker image Dockerfile:35 Drop from COPY; inject at runtime; rotate 0a
SEC-DAPI-04 designer-api 🟡 Medium G11 CORS wildcard + malformed header server.js:8-15 One configured cors(); delete manual header 0
SEC-DAPI-07 designer-api 🟡 Medium G1 SQL injection in interpolated queries routes/routes.js:1499… Parameterise; extend DB wrapper 0
SEC-DAPI-05 designer-api 🟡 Medium G13 50 MB body limit server.js:17 Global 256kb; per-route override 1
SEC-DAPI-10 designer-api 🟡 Medium G12 No rate limiting routes/routes.js:171 express-rate-limit; trust proxy 0
SEC-DAPI-08 designer-api 🟡 Medium forEach over async (correctness) job_get_image.js:304 +17 for…of await / p-limit 0
SEC-DAPI-09 designer-api ⚪ Low G2 Hardcoded Polotno key (11×) routes/routes.js:1261… Move to env; rotate if secret 0a
SEC-DAPI-11 designer-api ⚪ Low G7 No app-layer security headers server.js Add helmet 1
SEC-DAPI-12 designer-api ⚪ Low G7 nginx.conf not audited nginx.conf Review; add headers; nginx -t 1
SEC-AAPI-01 Someli-admin-api 🟠 High G2 Hardcoded session secret server.js:11 Rotate; env; fail-fast; secure cookie 0a
SEC-AAPI-02 Someli-admin-api 🟠 High G2 Hardcoded Slack token routes/auth.js:20-21 Rotate; env; reduce scopes 0a
SEC-AAPI-10 Someli-admin-api 🟠 High G2 Committed Google OAuth secret conf/credentials.json Rotate; untrack; inject at deploy 0a
SEC-AAPI-03 Someli-admin-api 🟡 Medium G6 In-memory token revocation helper/revokeToken.js Persist in tRevokedToken 0
SEC-AAPI-04 Someli-admin-api 🟡 Medium G6 Tokens never expire helper/tokenGenerator.js:3 Set JWT_EXPIRES_IN + default 0
SEC-AAPI-05 Someli-admin-api 🟡 Medium G11 CORS wildcard server.js:75 Origin allow-list 0
SEC-AAPI-07 Someli-admin-api 🟡 Medium G4 Role gating in SQL only routes/auth.js:74 requireRole middleware per route 0
SEC-AAPI-08 Someli-admin-api 🟡 Medium G1 SQL injection via dynamic filters routes/auth.js:410 Parameterise; allow-list identifiers 0
SEC-AAPI-06 Someli-admin-api ⚪ Low Webhook exemption, no handlers server.js:19-25 Remove, or implement + verify signature 1
SEC-AAPI-09 Someli-admin-api ⚪ Low G13 150 MB body limit server.js:23 Lower to 1mb; add helmet 1
SEC-DASH-02 someli-dashboard-be 🟡 Medium G4 Auth-by-convention routes/index.js:24 In-router guard fails closed 0
SEC-DASH-01 someli-dashboard-be ⚪ Low G2 Fallback session secret change-me server.js:9 Remove unused session / fail-fast 1
SEC-DASH-03 someli-dashboard-be ⚪ Low G4 Mock auth deployable mock/middlewares/auth.js Gate behind explicit opt-in flag 1
SEC-DASH-04 someli-dashboard-be ⚪ Low G7 No standalone hardening server.js helmet + CORS + rate-limit 1
SEC-DASH-05 someli-dashboard-be ⚪ Low G15 Spurious/deprecated deps package.json Prune unused deps 1
SEC-ADMINUI-01 admin_console_R 🟡 Medium G5 Token in localStorage src/context/AuthContext.tsx:32 HttpOnly cookie (backend coord) 0
SEC-ADMINUI-03 admin_console_R 🟡 Medium G9 Lovable.dev script in production index.html:14-15 Remove from prod build / SRI-pin 0
SEC-ADMINUI-04 admin_console_R 🟡 Medium G8 No Content Security Policy (no CSP in repo) Add CSP (nginx/meta); report-only first 0
SEC-ADMINUI-11 admin_console_R 🟡 Medium G6 No 401 → logout src/services/api.ts:5-13 Clear session + redirect on 401 0
SEC-ADMINUI-02 admin_console_R ⚪ Low G2 Hardcoded Apptype src/services/api.ts:23 Use ENV.APP_TYPE 1
SEC-ADMINUI-05 admin_console_R ⚪ Low G9 No SRI index.html Resolve via -03; CI grep guard 1
SEC-ADMINUI-09 admin_console_R ⚪ Low G10 dangerouslySetInnerHTML unsanitised src/components/ui/chart.tsx:79 Keep config dev-defined; lint guard 1
SEC-ADMINUI-10 admin_console_R ⚪ Low G14 PII in localStorage AuthContext.tsx:67 Source from context; clear on logout 1
SEC-ADMINUI-06 admin_console_R ⚪ Low HMR overlay disabled (dev) vite.config.ts:18-21 Re-enable overlay 2
SEC-ADMINUI-07 admin_console_R ⚪ Low G15 lovable-tagger dev plugin vite.config.ts:25 Keep dev gate; verify not in dist/ 2
SEC-ADMINUI-08 admin_console_R ⚪ Low Minimal Vite fs.deny (dev) vite.config.ts:14-17 Broaden deny list; localhost only 2
SEC-DESUI-01 Someli-Designer 🟠 High G4 Auth guard per-page not global nuxt.config.js:49-51 Global ['auth','axios']; guest opt-out 0
SEC-DESUI-02 Someli-Designer 🟡 Medium G5 Token in localStorage/cookie nuxt.config.js:65-76 HttpOnly cookie (backend coord) 0
SEC-DESUI-03 Someli-Designer 🟡 Medium G6 No server-side logout nuxt.config.js:72 Add POST /logout; wire $auth.logout() 0
SEC-DESUI-04 Someli-Designer 🟡 Medium G4 Role gating UI-only components/Navbar.vue Enforce server-side; FE guard 1
SEC-DESUI-06 Someli-Designer 🟡 Medium G8 No CSP (nginx.conf uncommitted) Dockerfile:77 Commit nginx.conf; add CSP + headers 0
SEC-DESUI-05 Someli-Designer ⚪ Low G9 CDN scripts without SRI nuxt.config.js:16-20 Add integrity; self-host 1
SEC-DESUI-08 Someli-Designer ⚪ Low G2 Hardcoded Polotno key + localStorage designs polotno-editor/index.js:7 Env var; rotate; scope/clear storage 1
SEC-DESUI-09 Someli-Designer ⚪ Low G3 Client-side bcrypt hashing pages/login.vue:98,156-160 Remove; hash server-side; drop dep 1
SEC-DESUI-10 Someli-Designer ⚪ Low G10 v-html renders AI/PDF content pages/AI-Content.vue:38 +6 DOMPurify filter / {{ }} 1
SEC-DESUI-07 Someli-Designer ⚪ Low Bootstrap v2/v5 mismatch (UX) nuxt.config.js:16-20 Pick one Bootstrap line 1

Totals: 80 findings — 3 🔴 Critical, 20 🟠 High, 30 🟡 Medium, 27 ⚪ Low. (A few closely-related issues in someli-api are merged into single findings here — e.g. plaintext password storage + comparison, and the two forgeable-token issues — so this count is slightly lower than an un-deduplicated per-row tally.)

Recurring categories worth grouping into epics: hardcoded secrets [G2] spans 6 repos (and most Phase-0a tickets); missing headers/CSP/SRI [G7/G8/G9] span all front-facing services; SQL injection [G1] and CORS [G11] span the backends; insecure token storage/lifecycle [G5/G6] span every web app. Fixing each category with one shared pattern (then applying per repo) is the efficient path.