T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/generate-html-report.js:301
- Finding
- Stored HTML Injection in Generated Assessment Reports<![CDATA[ ## Vulnerability Details **File Location**: `scripts/generate-html-report.js:301-318` **Additional Locations**: `scripts/generate-html-report.js:333-344`, `scripts/generate-html-report.js:382-383`, `scripts/generate-html-report.js:566-567` **Vulnerability Type**: Stored HTML/JavaScript injection through unescaped report fields **Risk Level**: Medium ### Vulnerable Code ```js if (d.status === 'skipped') { return ` <div class="question-card skipped"> <div class="q-header"> <span class="q-num">Q${idx + 1}</span> <span class="q-dim">${d.id} ${name}</span> <span class="q-status skip-badge">${statusText}</span> </div> <p class="skip-reason">${isZH ? '原因' : 'Reason'}: ${d.skipReason || 'Required capability not available'}</p> </div>`; } const criteriaRows = (d.criteria || []).map(c => ` <tr> <td>${c.name}</td> <td>${(c.weight * 100).toFixed(0)}%</td> <td><span class="score-pill ${c.score >= 4 ? 'high' : c.score >= 3 ? 'mid' : 'low'}">${c.score}/5</span></td> <td class="justification">${c.justification || ''}</td> </tr>`).join(''); ``` Other unescaped metadata is inserted into the document: ```js return ` <div class="question-card"> <div class="q-header"> <span class="q-num">Q${idx + 1}</span> <span class="q-dim">${d.id} ${name}</span> <span class="q-diff">${diff} ×${d.multiplier}</span> <span class="q-score-badge" style="background:${d.adjScore >= 80 ? '#DCFCE7' : d.adjScore >= 60 ? '#FEF3C7' : '#FEE2E2'};color:${d.adjScore >= 80 ? '#166534' : d.adjScore >= 60 ? '#92400E' : '#991B1B'}">${d.adjScore.toFixed(1)}</span> <span class="q-status">${statusText}</span> </div> ``` ```js const html = `<!DOCTYPE html> <html lang="${lang}"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>${t.title} — ${data.sessionId}</title> ``` ```js <div class="header"> <h1>${t.title}</h1> <div class="meta"> <span>${t.session}: ${ ...[truncated 2986 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Apply context-appropriate encoding to every dynamic string, not only question and answer fields. 2. Use HTML text encoding for values inserted into element bodies: ```js function escapeHtml(value) { return String(value) .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } ``` 3. Encode attribute values separately and validate fields such as `lang`, `sessionId`, and dimension IDs against strict allowlists. 4. Validate input against the supplied JSON schema before report generation. Add: - String length limits. - Enumerations for language, status, confidence, level, and dimension IDs. - Numeric ranges for scores, weights, and multipliers. - A restrictive pattern such as `^[A-Za-z0-9_-]+$` for session identifiers. 5. Avoid constructing complex documents through raw template interpolation. A DOM builder or templating system with automatic escaping is preferable. 6. Add a restrictive Content Security Policy to generated reports, for example: ```html <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src data:"> ``` 7. Add regression tests for every report string field using payloads containing: - `<script>` elements - Event-handler attributes - Closing table and `details` tags - Single and double quotes - Encoded markup 8. Treat scoring justifications as untrusted because they may quote or summarize attacker-controlled examination content. ]]>
