T09 · Insecure Skill Coding Practices
Error
- Location
- src/ui/dashboard.html:715
- Finding
- Persistent Cross-Site Scripting in Project Navigation Can Compromise Gateway Credentials<![CDATA[ ## Vulnerability Details **File Location**: `src/ui/dashboard.html:464-477`, `src/ui/dashboard.html:715-723`; generated copy in `src/ui/dashboard.generated.ts:4` **Vulnerability Type**: Persistent cross-site scripting caused by unsafe JavaScript-context interpolation **Risk Level**: High ### Vulnerable Code ```javascript // AUTH is injected by the server when using the embedded gateway route. // When empty (standalone UI server), the token is read from localStorage const AUTH_INJECTED = ''; function getStoredToken() { return AUTH_INJECTED || localStorage.getItem('orchard-token') || ''; } function setStoredToken(t) { localStorage.setItem('orchard-token', t.trim()); } function clearStoredToken() { localStorage.removeItem('orchard-token'); } ``` ```javascript els.sidebarProjects.innerHTML = state.projects.length ? state.projects.map((project) => ` <div class="sidebar-item ${activeProjectId === project.id ? 'active' : ''}" onclick="navigate('#project/${encodeURIComponent(project.id)}')"> <div class="sidebar-item-main"> <div class="sidebar-item-title">${escapeHtml(project.name)}</div> <div class="sidebar-item-meta">${percent(project.completion_score)}% complete</div> </div> ${badge(projectStatus(project))} </div> `).join('') : '<div class="empty">No projects yet.</div>'; ``` ### Technical Analysis Project IDs originate from authenticated API input and are persisted in SQLite. The dashboard later inserts each ID into an inline JavaScript event handler assigned through `innerHTML`. `encodeURIComponent()` is URL encoding, not JavaScript-string escaping. In particular, it does not encode apostrophes. A crafted project ID can therefore terminate the single-quoted argument passed to `navigate()` and introduce additional JavaScript. An attacker can use an alphanumeric Base64 payload with an expression such as `eval(atob(...))`, avoiding many characters that `encodeURIComponent()` would otherwise encode. The issue is more ...[truncated 1836 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Remove all inline event handlers and avoid embedding database values into executable JavaScript: ```javascript const row = document.createElement('div'); row.className = 'sidebar-item'; row.addEventListener('click', () => { navigate(`#project/${encodeURIComponent(project.id)}`); }); ``` 2. Construct untrusted content using `textContent`, DOM methods, or a template system with contextual escaping rather than `innerHTML`. 3. Apply separate encoders for HTML text, HTML attributes, URLs, and JavaScript strings; HTML escaping alone is not sufficient for executable contexts. 4. Stop storing a gateway bearer token in `localStorage`. Prefer an `HttpOnly`, `Secure`, and `SameSite=Strict` cookie or a short-lived, narrowly scoped UI session. 5. Add a restrictive Content Security Policy that disallows inline scripts and event handlers, for example with nonce-based scripts and no `unsafe-inline`. 6. Regenerate `src/ui/dashboard.generated.ts` after correcting `dashboard.html`. 7. Add security tests using project IDs containing apostrophes, parentheses, HTML metacharacters, and encoded JavaScript payloads. ]]>
