T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/generate.py:47
- Finding
- Arbitrary JavaScript Injection Through Crafted Asset Filenames<![CDATA[ ## Vulnerability Details **File Location**: `scripts/generate.py:47-49, 64-69`; resulting unsafe DOM sinks in `assets/template/index.html:124-132, 176-178` **Vulnerability Type**: JavaScript injection caused by missing output encoding **Risk Level**: High ### Vulnerable Code ```python # scripts/generate.py:47-49 images = sorted([f for f in os.listdir(cat_dir) if f.endswith(('.png', '.jpg', '.jpeg'))]) config['categories'].append({ 'name': category, 'assets': [f'assets/{category}/{img}' for img in images] }) ``` ```python # scripts/generate.py:64-69 # Generate array string assets_array = ', '.join([f"'{a}'" for a in assets]) # Replace in HTML (find variable like hairAssets = [...]) pattern = rf"({cat_name}Assets\s*=\s*\[)[^\]]*(\])" replacement = rf"\1{assets_array}\2" html = re.sub(pattern, replacement, html, flags=re.IGNORECASE) ``` The generated values subsequently reach HTML-parsing sinks: ```javascript // assets/template/index.html:124-132 container.innerHTML = ""; assets.forEach((src, index) => { const btn = document.createElement("button"); btn.className = "item-btn " + category; btn.setAttribute("data-index", index); btn.innerHTML = "<img src='" + src + "'>"; btn.onclick = () => selectItem(category, src, index); }); ``` ```javascript // assets/template/index.html:176-178 if (category === "hair") document.getElementById("layer-front-hair").innerHTML = "<img src='" + src + "'>"; else if (category === "dress") document.getElementById("layer-dress").innerHTML = "<img src='" + src + "'>"; else if (category === "shoes") document.getElementById("layer-shoes").innerHTML = "<img src='" + src + "'>"; ``` ### Technical Analysis The generator treats filenames from the caller-controlled input directory as trusted JavaScript source text. It checks only whether each filename ends in `.png`, `.jpg`, or `.jpeg`. It does not restrict metacharacters or escape the filename before placing it inside a single-quoted JavaScript stri ...[truncated 2031 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Serialize JavaScript data with a real serializer instead of constructing source strings: ```python assets_array = json.dumps(assets, ensure_ascii=False) ``` 2. Replace the entire array expression using a replacement function so that backslashes in serialized data are not interpreted as regular-expression replacement references: ```python pattern = rf"({re.escape(cat_name)}Assets\s*=\s*)\[[^\]]*\]" html = re.sub( pattern, lambda match: match.group(1) + json.dumps(assets, ensure_ascii=False), html, flags=re.IGNORECASE, ) ``` 3. Restrict asset filenames to an explicit safe format, for example: ```python SAFE_FILENAME = re.compile(r"^[A-Za-z0-9._-]+$") ``` Reject any filename that does not match rather than silently copying it. 4. Check extensions case-insensitively and verify actual file types using image decoding or magic-byte inspection. Extension validation alone does not establish that a file is an image. 5. Remove the `innerHTML` image sinks. Construct elements through DOM APIs: ```javascript const image = document.createElement("img"); image.src = src; btn.replaceChildren(image); ``` 6. Add regression tests using filenames containing quotes, backslashes, newlines, angle brackets, Unicode separators, and regular-expression replacement metacharacters. Confirm that generated output remains syntactically valid and that none of those values can create script or markup nodes. ]]>
