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 fourv-htmlbindings with no sanitisation. TheformatProfileContentobject branch (line 1447) additionally interpolates raw server-suppliedkeyandvaluestrings into an HTML template literal. - Why it matters: The content in
message.textandmessage.profilecomes 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 tolocalStorage.someliAIChatState, so a single poisoned response survives page reloads. - Fix: Install
dompurifyand wrap the return value offormatMessage:Inimport DOMPurify from 'dompurify' // end of formatMessage: return DOMPurify.sanitize(result, { ALLOWED_TAGS: ['strong','em','br','h3','span'], ALLOWED_ATTR: ['style'] })formatProfileContent, HTML-escapeformattedKeyandvaluebefore interpolation:
C2. middleware/axios.js registers a new request interceptor on every route navigation¶
- File:
middleware/axios.js:6-17; registered atnuxt.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 theJSON.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.jsand add it tonuxt.config.js → plugins. Keep only thecurrentUrlcookie 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()referencesstartDate,endDate, andcustomDates— local variables that belong togetCustomDates()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 throwsReferenceError: startDate is not defined. Week-backwards navigation on the dashboard is completely broken. - Fix: Delete lines 506-531 from
decreaseByWeek. The localstart/endmoment objects computed at lines 491-492 are the correct variables to pass tothis.getCustomDates(...).
H2. SettingsBrandKit.vue uses Vue 3 beforeUnmount — click listener for color picker is never removed¶
- File:
components/SettingsBrandKit.vue:524, 538 - What:
mountedregistersdocument.addEventListener('click', this.handleClickOutsideColorPicker). The cleanup is inbeforeUnmount()— 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()tobeforeDestroy().
H3. store/api.js:fetchImageLibrary sends undefined as the account_Id header¶
- File:
store/api.js:431 - What: The action uses
this.accountIdfor the header. In a Vuex actionthisis the store instance — it has noaccountIdproperty. Every other paginated action in this file correctly usesthis.$auth.user.accountId. - Why it matters: The backend receives no
account_Idheader 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 skipsapi/clearApiCache.AccountSwitcher's account-switch path also skips it. TheapiVuex 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')inlogOut()before the redirect. Add the same call inAccountSwitcher.switchAccount()beforethis.switchUrl(account.id).
H5. MyContent.vue defines beforeDestroy twice — first definition silently overwritten¶
- File:
components/MyContent.vue:483-487and1023-1025 - What: The same
export default {}object definesbeforeDestroy()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 thedocument.click → closeDropdownlistener, never executes. - Why it matters: Every mount of
MyContentpermanently adds adocumentclick listener. Repeated navigations accumulate stale handlers that callcloseDropdown(which touches component state) on destroyed Vue instances. - Fix: Merge both hooks: 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
fetchcall includes onlyContent-Type: application/json. It does not attach thetokencookie oraccountIdheader that the axios middleware injects for all other authenticated requests. - Why it matters: If
/api/profile/streamrequires 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
fetchheaders, or proxy the call throughthis.$axios.post(...)so the existing interceptor chain applies.
M2. switchpostroom.vue hashchange listener never removed¶
- File:
components/switchpostroom.vue:82-84 - What:
mountedregisterswindow.addEventListener('hashchange', () => { this.checkHash() })using an anonymous arrow function. The component has nobeforeDestroyhook 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.logcalls. Theconsole.errorat line 604 (invalid date format) is acceptable to retain.
M4. window.open(item.igPermalink, "_blank") opens unvalidated server URL¶
- File:
components/Dashboard/socialcard/PostList.vue:244 - What:
item.igPermalinkfrom the API is passed directly towindow.openwithout scheme validation. - Why it matters: A
javascript:scheme value (from a poisoned post record) executes arbitrary JS. Also applies towindow.opencalls using server-suppliedresult.redirectURLinCircleNetworkModal.vue:142andEaTopbar.vue:1017. - Fix: Validate the scheme before opening:
Add
if (/^https?:\/\//.test(item.igPermalink)) { window.open(item.igPermalink, '_blank', 'noopener,noreferrer') }noopener,noreferrerto allwindow.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 usev-htmlfor a plain text message. - Fix:
{{ error.message }}
L2. GTM debug: true hardcoded in production nuxt.config.js¶
- File:
nuxt.config.js:271 - What:
@nuxtjs/gtmis configured withdebug: trueunconditionally, 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_KEYis referenced client-side. The key is not inDockerfilebuild args or theuat_app_ecs.ymlsecrets block, so it evaluates toundefinedin all production builds. The LinkPreview API call silently fails for all users. - Fix: Add
LINKPREVIEW_API_KEYtoDockerfileARG/ENV and the CI workflow secrets extraction (follow thePADDLE_CLIENT_TOKENpattern).
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.