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.js→6929fe58 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 → pluginsand is not imported anywhere inpages/,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):
- Rotate the AWS access key immediately in AWS IAM. Do this before any other change — the rotation cannot wait.
- Audit AWS CloudTrail for this access-key-id since 2022-08-01 to identify any unauthorised use.
- Delete
plugins/aws_sdk.jsfrom the working tree. - Run
git-filter-repoorbfgto rewrite the keys out of git history (note: this is destructive history rewriting; coordinate with all clones). - After rewrite, force-push and require all engineers to re-clone.
- Add a pre-push secret-scanner (
gitleaksin 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¶
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:
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):
- 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. - Move inline scripts to nonce-based CSP if/when feasible (requires per-request nonce generation, hard with
nuxt generate— easier withnuxt start). - Phase out
unsafe-inlineby 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):
- Install
dompurify(~12 KB gzipped). - In
helpers/helper.js, exposesafeHtml(s)=DOMPurify.sanitize(s, { … }). - Replace each
v-htmlwithv-html="safeHtml(...)"or refactor to interpolation. - 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 HTTPSX-Frame-Options/frame-ancestors(CSP) — clickjackingX-Content-Type-Options: nosniff— MIME sniffingReferrer-Policy— referrer leakagePermissions-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— publicSTRIPE_PUBLIC_KEY_LIVE/STRIPE_PUBLIC_KEY_TEST— public by Stripe designPADDLE_CLIENT_TOKEN/PADDLE_TEST_CLIENT_TOKEN— public by Paddle designFACEBOOK_CLIENT_ID,TWITTER_CLIENT_ID,TWITTER_CUSTOMER_KEY— public OAuth client IDsGTM_ID,CLARITY_PROJECT_ID— public analytics IDsYOUTUBE_API_KEY— used only byapi/index.jsserver middleware, which runs in the Nuxt Node process, not the browser. Not in the client bundle (verify viagrep "YOUTUBE_API_KEY" dist/_nuxt/*.jsafter a fresh build). Confirm.GOOGLE_API_KEY— may 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¶
Status: Clean. No eval / new Function usage in project code.
11. WC-SEC-10 — Token in URL¶
[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:
- 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.
- 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.
- 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-htmluse sites (DOMPurify or refactor to interpolation). - Set up Dependabot security-only auto-PRs.
- Run
yarn auditand triage critical/high vulns; create issues for each. - Add
gitleaksor equivalent secret-scanner to CI. - Move session token to httpOnly cookie (requires backend coordination).
- Migrate
aws-sdkv2 → 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 (
stripeNode SDK,@sendgrid/mail,passport*if not needed in build). - Consolidate auth header to one canonical format (drop
tokenand rawAuthorizationin favour ofAuthorization: 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_KEYscope 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.