Skip to content

someli-platform — code inspection

Audit date: 2026-05-17. Lens: concrete code-level issues not already covered by someli-doc/audit/someli-platform/ doc set.

Summary

15 findings: 2 critical, 5 high, 4 medium, 4 low. Two dominant themes: (1) server-supplied content rendered via v-html without sanitisation — the chatbot being the worst case with AI-generated text rendered directly as HTML across four template locations; (2) lifecycle management failures — a router middleware that accumulates axios interceptors on every navigation, a Vue 2 component using the Vue 3 beforeUnmount hook (silently ignored), and a duplicate beforeDestroy definition whose first copy is silently overwritten. A third theme: the api Vuex cache is not cleared on logout or account switch, letting one user's data bleed into the next session.

Critical

C1. Chatbot renders AI server responses as unsanitised HTML — stored XSS

  • File: components/Chatbot.vue:48, 79, 208, 214, 1428, 1447
  • What: formatMessage() converts markdown-like tokens from the AI SSE stream into <strong>, <em>, <h3> tags and returns the result to four v-html bindings with no sanitisation. The formatProfileContent object branch (line 1447) additionally interpolates raw server-supplied key and value strings into an HTML template literal.
  • Why it matters: The content in message.text and message.profile comes from the backend SSE endpoint /api/profile/stream. If the backend returns attacker-controlled text — via prompt injection, a compromised AI provider, or a misconfigured response — any <script>, onerror=, or <img src=x> executes in the user's session. Chat history is persisted to localStorage.someliAIChatState, so a single poisoned response survives page reloads.
  • Fix: Install dompurify and wrap the return value of formatMessage:
    import DOMPurify from 'dompurify'
    // end of formatMessage:
    return DOMPurify.sanitize(result, { ALLOWED_TAGS: ['strong','em','br','h3','span'], ALLOWED_ATTR: ['style'] })
    
    In formatProfileContent, HTML-escape formattedKey and value before interpolation:
    const esc = (s) => String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;')
    profileHtml += `<div class="profile-item"><strong>${esc(formattedKey)}:</strong> <span>${esc(value)}</span></div>`
    

C2. middleware/axios.js registers a new request interceptor on every route navigation

  • File: middleware/axios.js:6-17; registered at nuxt.config.js:242
  • What: This file is a global router middleware. It calls $axios.interceptors.request.use(...) unconditionally on every navigation. Axios never ejects interceptors automatically — the internal array grows by one per page transition.
  • Why it matters: After 50 navigations, every /auth/* request fires the token-injection callback 50 times. More critically, if the JSON.parse(cookie?.usedetail) at line 12 ever throws (malformed cookie, base64 = padding before URL-encoding is applied), subsequent navigations add a new interceptor before the broken one, permanently corrupting the header injection for the rest of the session. This is also an unbounded memory leak.
  • Fix: Move interceptor registration to a Nuxt plugin (runs once, not per navigation). Extract the current interceptor body into plugins/axios-auth.js and add it to nuxt.config.js → plugins. Keep only the currentUrl cookie write in the slimmed router middleware.

High

H1. NewDashboard.vue:decreaseByWeek() throws ReferenceError at runtime — "previous week" button broken

  • File: components/Dashboard/NewDashboard.vue:506-531
  • What: decreaseByWeek() references startDate, endDate, and customDates — local variables that belong to getCustomDates() and do not exist in this method's scope. Lines 506-531 are debug code copy-pasted from the wrong method.
  • Why it matters: Clicking the left arrow (< button, mapped to @click="decreaseByWeek()") immediately throws ReferenceError: startDate is not defined. Week-backwards navigation on the dashboard is completely broken.
  • Fix: Delete lines 506-531 from decreaseByWeek. The local start/end moment objects computed at lines 491-492 are the correct variables to pass to this.getCustomDates(...).

H2. SettingsBrandKit.vue uses Vue 3 beforeUnmount — click listener for color picker is never removed

  • File: components/SettingsBrandKit.vue:524, 538
  • What: mounted registers document.addEventListener('click', this.handleClickOutsideColorPicker). The cleanup is in beforeUnmount() — the Vue 3 lifecycle hook. In this Vue 2 app it is silently ignored.
  • Why it matters: Every mount of SettingsBrandKit (which lives in a settings tab) permanently adds a document-level click handler that is never removed.
  • Fix: Rename beforeUnmount() to beforeDestroy().

H3. store/api.js:fetchImageLibrary sends undefined as the account_Id header

  • File: store/api.js:431
  • What: The action uses this.accountId for the header. In a Vuex action this is the store instance — it has no accountId property. Every other paginated action in this file correctly uses this.$auth.user.accountId.
  • Why it matters: The backend receives no account_Id header and likely returns empty results or data from the wrong account. The image library panel is silently broken for all users.
  • Fix: headers: { account_Id: this.$auth.user.accountId }

H4. API cache not cleared on logout or account switch

  • File: components/Layout/EaTopbar.vue:1262-1275; components/AccountSwitcher.vue:228-248
  • What: EaTopbar.logOut() clears cookies and localStorage but skips api/clearApiCache. AccountSwitcher's account-switch path also skips it. The api Vuex module retains all cached data (subscription, user details, settings, posts, approved dates) for up to 5 minutes after the session boundary.
  • Why it matters: If a different user logs in during the same browser session, or an admin switches between impersonated accounts, the second context sees the first context's cached data until TTL expiry. Subscription tier, post counts, and settings can all be wrong.
  • Fix: Add await this.$store.dispatch('api/clearApiCache') in logOut() before the redirect. Add the same call in AccountSwitcher.switchAccount() before this.switchUrl(account.id).

H5. MyContent.vue defines beforeDestroy twice — first definition silently overwritten

  • File: components/MyContent.vue:483-487 and 1023-1025
  • What: The same export default {} object defines beforeDestroy() at line 483 and again at line 1023. In a JS object literal, the second key overwrites the first. Only the hook at line 1023 runs. The hook at 483, which removes the document.click → closeDropdown listener, never executes.
  • Why it matters: Every mount of MyContent permanently adds a document click listener. Repeated navigations accumulate stale handlers that call closeDropdown (which touches component state) on destroyed Vue instances.
  • Fix: Merge both hooks:
    beforeDestroy() {
      document.removeEventListener('click', this.closeDropdown)
      this.stopStatusCheck()
      document.removeEventListener('mousedown', this.handleClickOutside)
    }
    
    Delete the definitions at both lines 483 and 1023 and replace with the merged version.

Medium

M1. Chatbot SSE stream sends no authentication token

  • File: components/Chatbot.vue:353-358
  • What: The streaming fetch call includes only Content-Type: application/json. It does not attach the token cookie or accountId header that the axios middleware injects for all other authenticated requests.
  • Why it matters: If /api/profile/stream requires authentication, all chatbot requests silently fail (the error handling suppresses the error message in many code paths). If the endpoint does not require authentication, the chatbot is reachable by unauthenticated callers with only a session ID guess.
  • Fix: Read the token from the cookie and attach it to the fetch headers, or proxy the call through this.$axios.post(...) so the existing interceptor chain applies.

M2. switchpostroom.vue hashchange listener never removed

  • File: components/switchpostroom.vue:82-84
  • What: mounted registers window.addEventListener('hashchange', () => { this.checkHash() }) using an anonymous arrow function. The component has no beforeDestroy hook and cannot reference the arrow function for removal.
  • Why it matters: Each mount adds a permanent window-level event listener. The anonymous function closes over this, preventing the component instance from being garbage-collected after destroy.
  • Fix: Store a named reference and remove it in beforeDestroy.

M3. 22 console.log calls shipped to production in NewDashboard.vue

  • File: components/Dashboard/NewDashboard.vue:225, 412, 420, 434, 439, 443, 447, 453, 464, 470, 494, 500, 510, 523, 550, 556, 598, 625, 648, 665, 682, 687
  • What: Debug logs throughout date-navigation and social-account selection methods expose internal state: defaultprovider, selectedIds, date range values, and provider IDs in the browser console.
  • Fix: Remove all console.log calls. The console.error at line 604 (invalid date format) is acceptable to retain.
  • File: components/Dashboard/socialcard/PostList.vue:244
  • What: item.igPermalink from the API is passed directly to window.open without scheme validation.
  • Why it matters: A javascript: scheme value (from a poisoned post record) executes arbitrary JS. Also applies to window.open calls using server-supplied result.redirectURL in CircleNetworkModal.vue:142 and EaTopbar.vue:1017.
  • Fix: Validate the scheme before opening:
    if (/^https?:\/\//.test(item.igPermalink)) {
      window.open(item.igPermalink, '_blank', 'noopener,noreferrer')
    }
    
    Add noopener,noreferrer to all window.open(..., '_blank') calls throughout the codebase to prevent reverse tabnapping.

Low / nits

L1. layouts/error.vue:8 renders error.message via v-html

  • File: layouts/error.vue:8
  • What: The Nuxt error layout uses <p v-html="error.message">. Error messages are framework-generated in practice, so XSS risk is low, but there is no reason to use v-html for a plain text message.
  • Fix: {{ error.message }}

L2. GTM debug: true hardcoded in production nuxt.config.js

  • File: nuxt.config.js:271
  • What: @nuxtjs/gtm is configured with debug: true unconditionally, producing verbose console output for GTM events in production.
  • Fix: debug: process.env.NODE_ENV !== 'production'

L3. LINKPREVIEW_API_KEY in client bundle but absent from build pipeline

  • File: components/AddWebsiteModal.vue:242
  • What: process.env.LINKPREVIEW_API_KEY is referenced client-side. The key is not in Dockerfile build args or the uat_app_ecs.yml secrets block, so it evaluates to undefined in all production builds. The LinkPreview API call silently fails for all users.
  • Fix: Add LINKPREVIEW_API_KEY to Dockerfile ARG/ENV and the CI workflow secrets extraction (follow the PADDLE_CLIENT_TOKEN pattern).

L4. Hardcoded analytics IDs as nuxt.config fallbacks

  • File: nuxt.config.js:26, 37, 269
  • What: Microsoft Clarity (u1v818dob2), FirstPromoter (27t8fy1e), and GTM (GTM-KBZLX362) IDs are hardcoded as fallback values. Staging environments without the env var set silently report analytics to the production projects.
  • Fix: Use empty string fallbacks and guard on non-empty values, or remove the fallbacks entirely and let the tracking scripts no-op when the ID is absent.

Cross-cutting observations

Chatbot XSS surface is wide. formatMessage is called in four separate render paths (lines 48, 79, 208, 214). The profile object branch at line 1447 is a separate, unsanitised interpolation. Any fix must cover all five code paths.

store/api.js cache-coherence is incompletely enforced. The documentation in 05-state-management.md correctly mandates clearApiCache on mutations, but the two most critical session boundaries (logout, account switch) both miss it. A shared endSession() action that chains $auth.logout, clearApiCache, cookies.removeAll, and localStorage.clear would enforce the contract in one place.

middleware/axios.js is structurally misplaced. Router middleware is not the right place for one-time interceptor registration. Converting it to a plugin is a small change with measurable correctness and performance benefits. The parseCookies helper defined in this file is also independently reimplemented in multiple polotno-editor/ files — a shared utility in helpers/ would eliminate the duplication.

NewDashboard.vue needs a cleanup pass before the dashboard is considered stable. The combination of a ReferenceError in a primary navigation handler and 22 debug console.log calls indicates this component was committed while development was incomplete.