T09 · Insecure Skill Coding Practices
Error
- Location
- generate-book-article.py:121
- Finding
- Unescaped JSON Content Permits Stored HTML and JavaScript Injection<![CDATA[ ## Vulnerability Details **File Location**: `generate-book-article.py`, lines 121–122; additional affected sinks at lines 133, 207–225, and 296 **Vulnerability Type**: Stored HTML/JavaScript injection **Risk Level**: High ### Vulnerable Code ```python quote_parts.append( QUOTE_ITEM_TEMPLATE.replace('{INDEX}', idx).replace('{TEXT}', text) ) ``` Additional vulnerable interpolation points include: ```python def build_gold_quote_html(gq: dict) -> str: """为一条金句生成独立 HTML 块。""" text = gq.get('text', '') source = gq.get('source', '') if source: return GOLD_QUOTE_BLOCK_TEMPLATE.replace('{TEXT}', text).replace('{SOURCE}', source) else: return GOLD_QUOTE_BLOCK_NO_SOURCE_TEMPLATE.replace('{TEXT}', text) ``` ```python title = data.get('title', '') if data.get('title_line2'): line2 = data['title_line2'] line2 = (line2 .replace('{count}', str(total_cards)) .replace('{all_quotes}', str(total_all_quotes)) .replace('{quotes}', str(total_quotes))) title += '<br>' + line2 html = html.replace('{{TITLE}}', title) html = html.replace('{{SUBTITLE}}', data.get('subtitle', '')) intro = data.get('intro_paragraphs', []) html = html.replace('{{INTRO_PARAGRAPH_1}}', intro[0] if len(intro) > 0 else '') html = html.replace('{{INTRO_PARAGRAPH_2}}', intro[1] if len(intro) > 1 else '') ``` ```python html = f'<!DOCTYPE html>\n<html lang="zh-CN">\n<head>\n<meta charset="utf-8">\n<meta name="viewport" content="width=600">\n<title>{data.get("title", "")}</title>\n</head>\n<body style="margin:0;padding:0;">\n{html}\n</body>\n</html>' ``` ### Technical Analysis The generator treats JSON values as trusted HTML and inserts them directly into HTML templates through string replacement and f-string interpolation. It does not apply HTML escaping, contextual output encoding, or markup sanitization. The affected data includes card numbers, titles, descriptions, quote text, quote sources, article tit ...[truncated 2766 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Apply contextual HTML escaping to every value originating from JSON: ```python from html import escape def escape_text(value) -> str: return escape(str(value), quote=True) ``` 2. Escape values before inserting them into text, attribute, or `<title>` contexts: ```python safe_text = escape_text(q.get('text', '')) safe_index = escape_text(q.get('index', f'{j + 1:02d}')) quote_parts.append( QUOTE_ITEM_TEMPLATE .replace('{INDEX}', safe_index) .replace('{TEXT}', safe_text) ) ``` 3. Do not preserve arbitrary HTML merely to support line breaks. Instead, escape the entire value and then convert only an explicitly supported representation: ```python def escape_with_breaks(value) -> str: safe = escape(str(value), quote=True) return safe.replace('\r\n', '\n').replace('\r', '\n').replace('\n', '<br>') ``` If backward compatibility with literal `<br>` is required, normalize and allow only exact `<br>`, `<br/>`, or `<br />` tokens after escaping. Do not use a broad tag-removal regular expression as a sanitizer. 4. Use a template engine with automatic escaping enabled rather than constructing HTML through unrestricted `str.replace()` operations. Treat any deliberately safe HTML fragment as a separate, explicitly typed value. 5. Add strict schema validation: - Require expected types for all fields. - Reject objects or arrays where strings are expected. - Validate `after_card` as a bounded non-negative integer. - Impose reasonable length and collection-size limits. - Reject unsupported fields or markup where practical. 6. Add regression tests covering payloads in every interpolated field, including: ```html </title><script>alert(1)</script> <img src=x onerror=alert(1)> <a href="javascript:alert(1)">link</a> ``` Tests should confirm that these values appear only as inert text in the generated document while supported line breaks continue to render correctly. 7. Consider applying a res ...[truncated 157 chars]
