Skip to content

Security

Status: Audit v0.1 (2026-05-09).

This document enumerates the security posture of the web client. Findings are organised roughly by OWASP Top 10 (2021) categories where applicable.

Critical findings (TL;DR)

ID Finding Severity OWASP
WC-SEC-1 AWS access keys hardcoded in plugins/aws_sdk.js committed since 2022-08-01 Critical A02 (Cryptographic Failures), A05 (Security Misconfig)
WC-AUTH-2 Session token stored in browser-readable cookies; XSS = account takeover High A07 (Identification & Authentication Failures)
WC-SEC-2 No Content-Security-Policy header; unsafe-inline scripts via __dangerouslyDisableSanitizers in nuxt.config.js High A05
WC-SEC-3 No Subresource Integrity on 14 third-party CDN scripts High A08 (Software & Data Integrity Failures)
WC-SEC-4 9 v-html use sites without sanitisation (incl. Chatbot rendering AI output) High A03 (Injection)
WC-SEC-5 Vue 2 / Nuxt 2 / Webpack 4 — all EOL; no CVE patches High A06 (Vulnerable & Outdated Components)
WC-SEC-6 No automated dependency-update pipeline (no Dependabot/Renovate) High A06
WC-AUTH-4 Frontend-trusted roleType in cookies; user-editable Medium–High A01 (Broken Access Control)
WC-SEC-7 No security headers from nginx (HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy) Medium A05

Detail follows.

1. WC-SEC-1 — Hardcoded AWS access keys

plugins/aws_sdk.js:1-7:

const AWS = require('aws-sdk');
export const myBucket = 'someli';
export const s3 = new AWS.S3({
    accessKeyId: "AKIATWYW7A2V7KJWR6S6",
    region: 'us-west-2',
    secretAccessKey: "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"
});

Severity: Critical.

Status of the key:

  • Committed since the initial commit on 2022-08-01 (git log --oneline plugins/aws_sdk.js6929fe58 web_v1.1.16). Author: Suganya J.
  • In git history forever — the keys are reachable through git log even if the file is deleted today.
  • The plugin is NOT registered in nuxt.config.js → plugins and is not imported anywhere in pages/, components/, store/, etc. (grep -rln "from '@/plugins/aws_sdk'\|from '~/plugins/aws_sdk'" pages components store plugins). Webpack tree-shaking should keep it out of the production bundle.
  • Confirmed not present in current dist/_nuxt/*.js (grep -lE "AKIATWYW7A2V7KJWR6S6" dist/_nuxt/*.js → empty).

So the immediate exposure is: anyone who has had read access to this git repository at any point since 2022-08-01. This includes:

  • Any current or former engineer
  • Any contractor with repo access
  • Anyone the repo was ever shared with (e.g., cloud-IDE sessions, CI mirrors, forks)

Required actions (Phase 0a, this week):

  1. Rotate the AWS access key immediately in AWS IAM. Do this before any other change — the rotation cannot wait.
  2. Audit AWS CloudTrail for this access-key-id since 2022-08-01 to identify any unauthorised use.
  3. Delete plugins/aws_sdk.js from the working tree.
  4. Run git-filter-repo or bfg to rewrite the keys out of git history (note: this is destructive history rewriting; coordinate with all clones).
  5. After rewrite, force-push and require all engineers to re-clone.
  6. Add a pre-push secret-scanner (gitleaks in CI is the standard).

Action 4 is partially mitigated by the fact that AWS will accept the rotation and the old key won't work. But the leaked credential is in many git mirrors, in the GitHub-hosted form of the repo, in any past clones — it has effectively had ~3.5 years of exposure. Treat this as a breach for compliance purposes (GDPR Article 33 if any EU customer data was reachable; SOC 2 incident response triggers).

2. WC-AUTH-2 — Token in browser-readable cookies

(Cross-referenced from authentication-client.md § Finding WC-AUTH-2.)

Severity: High.

The session token (token cookie) and full user record (usedetail cookie) are set without HttpOnly. The auth strategy uses cookie-universal-nuxt, which does not produce httpOnly cookies. JS on the same origin — including the 9 v-html sites and the dozen+ CDN scripts — can read these.

XSS = account takeover. See authentication-client.md for mitigations.

3. WC-SEC-2 — No Content-Security-Policy

grep -nE "Content-Security-Policy" nuxt.config.js nginx.conf
# (empty)

The deployed nginx serves no CSP header; the SPA injects no <meta http-equiv="Content-Security-Policy"> either.

Consequence: any XSS in the app has unrestricted script execution. CDN scripts can do anything.

nuxt.config.js:121 also sets:

__dangerouslyDisableSanitizers: ['script']

This disables Nuxt's HTML-sanitisation of inline scripts in head.script. Combined with the inline scripts for Microsoft Clarity (nuxt.config.js:25-28), FirstPromoter (:30-39), and identity (:30-39), the app is committed to allowing inline scripts. A CSP that allows unsafe-inline is partial but better than no CSP.

Recommendation (Phase 0):

  1. Add a baseline CSP header on the nginx side, starting permissive (script-src 'self' 'unsafe-inline' <whitelisted CDNs>; frame-ancestors 'none'; base-uri 'self') and tightening over time.
  2. Move inline scripts to nonce-based CSP if/when feasible (requires per-request nonce generation, hard with nuxt generate — easier with nuxt start).
  3. Phase out unsafe-inline by moving inline analytics scripts to <script src="/analytics.js"> files.

4. WC-SEC-3 — No Subresource Integrity on CDN scripts

nuxt.config.js:23-119 declares 14 external CDN scripts (plus 4 local-static scripts and 2 inline scripts; ~20 total <script> declarations). The external CDN ones are:

URL Notes
https://www.clarity.ms/tag/... Inline-loader; downstream URL is dynamic
https://cdn.firstpromoter.com/fpr.js Affiliate tracking
https://js.stripe.com/v3/ Stripe (https-only)
https://ajax.googleapis.com/.../jquery.min.js jQuery
https://cdn.jsdelivr.net/.../popper.min.js Popper
https://cdn.jsdelivr.net/.../bootstrap.min.js Bootstrap 5.2
https://kit.fontawesome.com/... FontAwesome
https://cdn.jsdelivr.net/.../bootstrap.bundle.min.js Bootstrap 5.2 (duplicate)
https://cdn.jsdelivr.net/.../popper.min.js Popper 1.16 (older variant)
https://cdn.jsdelivr.net/.../bootstrap.bundle.min.js Bootstrap 5.0.2 (third copy)
https://flagmeister.github.io/elements.flagmeister.min.js Geo widget
https://cdnjs.cloudflare.com/.../slick.js Slick carousel
https://cdnjs.cloudflare.com/.../jquery.validate.min.js jQuery validate
https://cdn.paddle.com/paddle/v2/paddle.js Paddle

None of these have integrity="sha384-..." attributes (grep -nE "integrity:" nuxt.config.js → empty).

The local-static scripts (/owl.carousel.min.js, /common.js, /universal.tag.js, /identity.js) and the 2 inline scripts (Clarity init wrapper at line 26, FirstPromoter init at lines 30-39) bring the total to 20 <script> declarations.

If any of these CDNs is compromised (or an attacker MITMs a customer's connection), arbitrary JavaScript executes with full access to the token cookie.

Recommendation (Phase 0a, <1 day): generate integrity hashes for the pinned versions and add them to the script declarations. Tools: https://www.srihash.org/.

flagmeister.github.io is particularly notable: it's served from GitHub Pages, which means whoever controls the flagmeister GitHub user can replace it with malicious code. SRI is the only defence.

5. WC-SEC-4 — v-html without sanitisation

grep -rEn "v-html" components | wc -l
# 9 (occurrences)
grep -rln "v-html" components | wc -l
# 4 (unique component files)
grep -rEn "v-html" pages | wc -l
# 0

The 9 occurrences are spread across 4 component files:

Component file Occurrences
components/Chatbot.vue 4
components/Layout/EaMainSection.vue 2
components/Forms/EaInputSwitch.vue 1
components/Dashboard/EaGauge.vue 2

Most concerning:

components/Chatbot.vue:1421-1434:

formatMessage(text) {
  let cleanedText = text.replace(/\s*\{buttons:\s*\[(.*?)\]\}/g, '')
                        .replace(/\s*\{buttons\s*:\s*\[(.*?)\]\}/g, '')
                        .replace(/\s*\{[^}]*buttons[^}]*\}/g, '')
  return cleanedText
    .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
    .replace(/\*(.*?)\*/g, '<em>$1</em>')
    .replace(/##\s*(.*?)$/gm, '<h3 style="...">$1</h3>')
    .replace(/•\s/g, '<span style="...">• </span>')
    .replace(/\n\n/g, '<br><br>')
    .replace(/\n/g, '<br>')
    .trim()
}

The function takes server-returned chat text and produces HTML, then v-html interpolates that into the DOM. Any <script> tag, <img onerror=...>, or javascript: URL in the text is preserved. The bot output is AI-generated and could be prompt-injected to return malicious HTML. Stored XSS via the chat backend is possible.

components/Chatbot.vue:1436-1453 → formatProfileContent:

profileHtml += `<div class="profile-item"><strong>${formattedKey}:</strong> <span>${value}</span></div>`

value is interpolated raw into HTML. Same issue.

Other v-html sites (Layout/EaMainSection.vue, Dashboard/EaGauge.vue, Forms/EaInputSwitch.vue) use v-html for layout text — likely controlled values, but still vector-able if a backend response feeds them.

Recommendation (Phase 0):

  1. Install dompurify (~12 KB gzipped).
  2. In helpers/helper.js, expose safeHtml(s) = DOMPurify.sanitize(s, { … }).
  3. Replace each v-html with v-html="safeHtml(...)" or refactor to interpolation.
  4. Add an ESLint rule (vue/no-v-html) to prevent future regressions.

6. WC-SEC-5 — EOL framework stack

Package Status
vue@2.7.16 EOL 2023-12-31
nuxt@2.18.1 EOL 2024-06-30
webpack@^4.46.0 Unsupported (Webpack 5 stable since 2020-10)
bootstrap-vue@^2.22.0 Last release Apr 2022

Implications:

  • No CVE patches will be issued for new vulnerabilities discovered in any of these.
  • Security tools (Snyk, Dependabot) will flag them as "out of support."
  • Recruiting and retaining engineers familiar with the EOL stack is increasingly difficult.

This is Finding WC-DEP-EOL in dependencies-inventory.md. The migration path is Vue 2 → Vue 3 + Nuxt 2 → Nuxt 3, treated as a multi-month Phase 2 effort in the roadmap.

7. WC-SEC-6 — No automated dependency-update pipeline

(Cross-referenced from dependencies-inventory.md § WC-DEP-7.)

Tool Wired?
Dependabot No (.github/dependabot.yml absent)
Renovate No
Snyk Not visible in repo
yarn audit in CI No (.github/workflows/dev_app.yml doesn't run it)

Dependencies are updated only when an engineer manually does so. Combined with the EOL stack, transitive CVE exposure is unbounded.

Recommendation (Phase 0, this month): enable Dependabot security-only auto-PRs. Even if framework upgrades can't be merged, point upgrades for transitive vulnerabilities should land automatically.

8. WC-SEC-7 — Missing security headers

The deployed nginx config (nginx.conf) sets none of:

  • Strict-Transport-Security (HSTS) — forces HTTPS
  • X-Frame-Options / frame-ancestors (CSP) — clickjacking
  • X-Content-Type-Options: nosniff — MIME sniffing
  • Referrer-Policy — referrer leakage
  • Permissions-Policy — feature gating

grep -nE "Content-Security-Policy|Strict-Transport-Security|X-Frame-Options|X-Content-Type-Options|Referrer-Policy|Permissions-Policy" nginx.conf → empty.

Recommendation (Phase 0):

add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://js.stripe.com https://cdn.paddle.com ..." always;

9. WC-SEC-8 — Public env vars in bundle

The frontend bundle contains values from the build-time .env. Public-by-design:

  • API_URL, APP_URL — public
  • STRIPE_PUBLIC_KEY_LIVE / STRIPE_PUBLIC_KEY_TEST — public by Stripe design
  • PADDLE_CLIENT_TOKEN / PADDLE_TEST_CLIENT_TOKEN — public by Paddle design
  • FACEBOOK_CLIENT_ID, TWITTER_CLIENT_ID, TWITTER_CUSTOMER_KEY — public OAuth client IDs
  • GTM_ID, CLARITY_PROJECT_ID — public analytics IDs
  • YOUTUBE_API_KEY — used only by api/index.js server middleware, which runs in the Nuxt Node process, not the browser. Not in the client bundle (verify via grep "YOUTUBE_API_KEY" dist/_nuxt/*.js after a fresh build). Confirm.
  • GOOGLE_API_KEYmay be in the bundle. [VERIFY-SEC-1] which scope is granted to this key.

Should not be in client bundle:

  • EXPRESS_SECRET — server-side only; [VERIFY-SEC-2] confirm not bundled.
  • TWITTER_CUSTOMER_SECRET, TWITTER_CLIENT_SECRET, FACEBOOK_CLIENT_SECRET — these are OAuth secrets which by definition must not leak to the client.

Recommendation (Phase 0a): verify the secrets are server-side only by grepping the production bundle:

yarn build
grep -nE "EXPRESS_SECRET|TWITTER_CUSTOMER_SECRET|TWITTER_CLIENT_SECRET|FACEBOOK_CLIENT_SECRET" .nuxt/dist/client/*.js dist/_nuxt/*.js

If any match: refactor the consumer of that secret to live in api/ server middleware, and rotate the secret.

10. WC-SEC-9 — eval / new Function usage

grep -rEn "eval\(|new Function\(" pages components store middleware plugins helpers
# (empty)

Status: Clean. No eval / new Function usage in project code.

11. WC-SEC-10 — Token in URL

grep -rEn "[?&]token=|window\.location\.search.*token" pages components middleware
# (review)

[VERIFY-SEC-3]: spot-check whether any flow puts the auth token in URL params (would be logged in browser history, server logs, referer headers).

12. WC-SEC-11 — Hardcoded telemetry / license keys

Not "secrets" but values worth knowing where they live:

Identifier Value Location Severity
AWS access key + secret AKIATWYW7A2V7KJWR6S6 / jtzq0F0... plugins/aws_sdk.js:4-6 Critical (see WC-SEC-1)
Polotno license key FXZvloSJvAe09-bdR9iC polotno-editor/index.js:14, polotno-editor/customTextSection/partials/textTemplate.js:11 Low — license keys are origin-bound
Microsoft Clarity ID u1v818dob2 nuxt.config.js:26 (default if env unset) Low — public ID
GTM container ID GTM-KBZLX362 nuxt.config.js:269 (default if env unset) Low — public ID
FirstPromoter cid 27t8fy1e nuxt.config.js:37 Low — public ID
FullStory org ID o-1C5EVB-na1 plugins/fullstory.js:11 (in unwired plugin) Low — public ID
Hotjar site id 3465582 plugins/hotjar.js:30 (in unwired plugin) Low — public ID

The Polotno license is the only one that has a non-zero "leak" risk; Polotno's license check binds to allowed origins at the SDK level, but the team should verify with Polotno's license terms that committing the key publicly is allowed. [VERIFY-SEC-4]: Polotno license terms — does it tolerate public exposure of the key?

13. WC-SEC-12 — Mixed content

grep -nE "src=[\"']http://" nuxt.config.js → none in head.script/head.link at the top of the file. All third-party scripts are HTTPS.

Recommendation: spot-check pages/, components/, and any inline <img src="http://..."> to confirm. Phase E verification.

14. WC-SEC-13 — Click-jacking

No X-Frame-Options / frame-ancestors (see WC-SEC-7). The app can be iframed by any site. Combined with the auth-token-in-cookie design, this is exploitable: an attacker page iframes the app, lures the user to click somewhere, gets the click to execute against the app.

Recommendation: add X-Frame-Options: DENY (Phase 0).

15. WC-SEC-14 — Stack trace exposure

Production builds do not ship source maps (ls dist/_nuxt/*.js.map → empty). Good for security; bad for observability (see observability.md).

The build does generate them in .nuxt/dist/ for development. Confirm they're not deployed: re-grep dist/ after a yarn build on CI.

16. WC-SEC-14b — Ad-blocker / extension breakage of payment SDKs

(Surfaced via existing dev doc ../10-troubleshooting.md § "Stripe / Paddle script not loading".)

Severity: Medium (loss of revenue, not a security exposure per se — but operationally critical).

Stripe.js (https://js.stripe.com/v3/) and Paddle.js (https://cdn.paddle.com/paddle/v2/paddle.js) are loaded as global CDN scripts via nuxt.config.js → head.script. Common ad-blockers and privacy extensions block these URLs, so checkouts silently fail for a non-trivial percentage of users.

The cross-cutting concern: combined with WC-OBS-1 (no error tracking), the team only learns about broken checkouts via support tickets — typically days after the user gave up.

Recommendations:

  1. Phase 0 (load on demand): move Stripe and Paddle SDK loads to the routes that actually need them (billing, payment). Reduces blast radius — non-checkout users are unaffected by the block.
  2. Phase 0 (detect + message): at checkout init, race the SDK load against a 2-3s timeout and a window-property check. If the SDK didn't load, show the user a clear "your browser extension is blocking our payment provider; please disable it for this domain" message instead of a stuck spinner.
  3. Phase 0 (instrument): once Sentry is in place, capture the SDK-load failure as a tracked event with a tag for ad-block detection. The team can then size the actual impact.

Cross-references: - observability.md § WC-OBS-1 (no error tracking) - performance.md § "Third-party blocking scripts" (cross-cutting issue with deferred loading) - enterprise-readiness.md § Risk Register WC-RISK-14

17. WC-SEC-15 — CORS

The frontend and backend run on different origins (frontend app.someli.ai, backend process.env.API_URL). Cookies use SameSite=None; Secure. The backend is therefore configured to accept cross-origin requests with credentials.

This is fine if the backend has a strict Access-Control-Allow-Origin whitelist (one specific frontend origin, not *). [VERIFY-SEC-5]: confirm backend CORS configuration.

18. Recommendations — phased

Phase 0a (this week)

  • Rotate the leaked AWS key (CRITICAL). Audit CloudTrail, then rotate.
  • Delete plugins/aws_sdk.js, rewrite git history.
  • Add Subresource Integrity to all CDN <script> tags.
  • Verify no OAuth client secrets in production bundle (grep).

Phase 0 (months 0–3)

  • Add baseline security headers in nginx (CSP, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy).
  • Sanitise the 9 v-html use sites (DOMPurify or refactor to interpolation).
  • Set up Dependabot security-only auto-PRs.
  • Run yarn audit and triage critical/high vulns; create issues for each.
  • Add gitleaks or equivalent secret-scanner to CI.
  • Move session token to httpOnly cookie (requires backend coordination).
  • Migrate aws-sdk v2 → v3 modules where actually needed; remove the v2 dep.
  • Move client-side role gating to per-navigation backend fetch.

Phase 1 (months 3–9)

  • Remove unused server-side npm packages from frontend (stripe Node SDK, @sendgrid/mail, passport* if not needed in build).
  • Consolidate auth header to one canonical format (drop token and raw Authorization in favour of Authorization: Bearer).
  • Add token-refresh path (response interceptor on 401).
  • Tighten CSP — eliminate unsafe-inline, move inline analytics to nonced files.
  • Consolidate UI library duplicates (3 emoji pickers, 3 date pickers, 3 toast libs, 2 cookie libs, 2 sortable libs).

Phase 2 (months 9–18)

  • Vue 2 → Vue 3 + Nuxt 2 → Nuxt 3 migration. Fundamentally fixes WC-SEC-5.
  • TypeScript adoption (or at least Zod schema validation at API service layer).

Phase 3 (months 18+)

  • Full SOC 2 Type I readiness (combined with backend).

19. Open questions

Tracked in ./verify-markers.md:

  • [VERIFY-SEC-1] GOOGLE_API_KEY scope and bundle inclusion
  • [VERIFY-SEC-2] EXPRESS_SECRET, OAuth client secrets — not in client bundle
  • [VERIFY-SEC-3] Token-in-URL spot check
  • [VERIFY-SEC-4] Polotno license terms (public key tolerance)
  • [VERIFY-SEC-5] Backend CORS whitelist configuration

Plus all [VERIFY-AUTH-*] markers from authentication-client.md.