T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/generate_service_bundle.py:500
- Finding
- Stored DOM-Based Cross-Site Scripting Through Dashboard Metadata<![CDATA[ ## Vulnerability Details **File Location**: `scripts/parse_sql_md.py:34-36, 79`; `scripts/build_dashboard_spec.py:123`; `scripts/generate_service_bundle.py:500-506` **Vulnerability Type**: Stored DOM-based cross-site scripting **Risk Level**: High ### Vulnerable Code Untrusted Markdown headings are accepted as query titles: ```python heading_match = HEADING_RE.match(line) if not in_sql and heading_match: level = len(heading_match.group(1)) title = heading_match.group(2).strip() section_stack = section_stack[: level - 1] section_stack.append(title) continue ``` The untrusted title is stored in the query catalog: ```python query = { "id": qid, "index": idx, "title": block_meta.get("title", section_title), "section": section_title, "datasource": block_meta.get("datasource", ""), "refresh": block_meta.get("refresh", ""), "chart_hint": block_meta.get("chart", "auto").strip().lower(), "filters": filters, "sql": sql_text, } ``` It is propagated into the dashboard specification without validation: ```python widget = { "id": f"widget_{qid}", "query_id": qid, "title": q.get("title") or qid, "chart": chart, ``` The generated service frontend inserts the title into an HTML parsing sink: ```javascript node.innerHTML = ` <div class="widget-head"> <div class="widget-title">${w.title || w.query_id}</div> <div class="widget-type">${w.chart || 'table'}</div> </div> <div class="widget-body"></div> `; ``` ### Technical Analysis The SQL Markdown file is an external input. Its headings and `title` metadata can contain arbitrary HTML because the parser does not validate or encode these fields. The resulting value passes through `query_catalog.json` and `dashboard.json`, is returned by the generated backend, and is interpolated directly into `Element.innerHTML`. Unlike `textContent`, `innerHTML` invokes the browser's HTML parser. Event-handler attributes or similar active marku ...[truncated 1724 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Do not use HTML-string interpolation for artifact-controlled values. 2. Construct the widget header using DOM APIs and assign all dynamic text with `textContent`: ```javascript const head = document.createElement('div'); head.className = 'widget-head'; const title = document.createElement('div'); title.className = 'widget-title'; title.textContent = String(w.title || w.query_id || ''); const type = document.createElement('div'); type.className = 'widget-type'; type.textContent = String(w.chart || 'table'); head.appendChild(title); head.appendChild(type); node.appendChild(head); const body = document.createElement('div'); body.className = 'widget-body'; node.appendChild(body); ``` 3. Apply context-appropriate encoding to every dynamic value before using any unavoidable HTML sink. 4. Validate metadata fields at ingestion. Reject control characters and enforce reasonable length limits, but do not treat validation as a substitute for safe output handling. 5. Add a restrictive Content Security Policy that blocks inline scripts and event handlers, for example: ```http Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'none' ``` 6. Add automated regression tests using payloads such as `<img src=x onerror=alert(1)>`, `<svg onload=alert(1)>`, and HTML-closing sequences in every Markdown metadata field. ]]>
