{"record":{"id":"7320e6203df77a07","repo":"666ghj/MiroFish","slug":"generated-svg-contains-an-unsafe-attribute-value","errorCode":null,"errorMessage":"generated SVG contains an unsafe attribute value","messagePattern":"generated SVG contains an unsafe attribute value","errorType":"exception","errorClass":"StarHistoryError","httpStatus":null,"severity":"critical","filePath":"scripts/star_history.py","lineNumber":1230,"sourceCode":"                \"href\": WATERMARK_LOGO_DATA_URI,\n            }\n            if element.attrib == avatar_attributes:\n                avatar_count += 1\n            elif element.attrib == watermark_attributes:\n                watermark_count += 1\n            else:\n                raise StarHistoryError(\"generated SVG contains an unreviewed image\")\n        for raw_name, value in element.attrib.items():\n            if raw_name.startswith(\"{\") or raw_name not in allowed_for_element:\n                raise StarHistoryError(\"generated SVG contains a forbidden attribute\")\n            if (\n                not value.isascii()\n                or \"\\\\\" in value\n                or \"/*\" in value\n                or \"*/\" in value\n                or any(ord(character) < 0x20 for character in value)\n            ):\n                raise StarHistoryError(\"generated SVG contains an unsafe attribute value\")\n            exact_values = exact_attribute_values.get(raw_name)\n            if exact_values is not None and value not in exact_values:\n                raise StarHistoryError(\"generated SVG contains an unsafe attribute value\")\n            if raw_name == \"href\":\n                if local_name != \"image\" or value not in {\n                    OWNER_AVATAR_DATA_URI,\n                    WATERMARK_LOGO_DATA_URI,\n                }:\n                    raise StarHistoryError(\"generated SVG contains an external resource\")\n            lowered = value.lower().replace(\" \", \"\")\n            if \"url(\" in lowered and lowered not in {\n                \"url(#xkcdify)\",\n                \"url(#clip-circle-title)\",\n            }:\n                raise StarHistoryError(\"generated SVG contains an external resource\")\n            if lowered.startswith((\"http:\", \"https:\", \"//\")):\n                raise StarHistoryError(\"generated SVG contains an external resource\")\n    if avatar_count != 1 or watermark_count != 1:","sourceCodeStart":1212,"sourceCodeEnd":1248,"githubUrl":"https://github.com/666ghj/MiroFish/blob/b5b53acc57189a4a42e44a23e149dc655c98fe82/scripts/star_history.py#L1212-L1248","documentation":"Raised by _validate_svg in scripts/star_history.py while auditing every attribute of the generated SVG. An attribute value failed the character-safety test: it must be pure ASCII, contain no backslash, no CSS comment delimiters (/* or */), and no control characters below 0x20. This is a hard invariant of the render pipeline: the SVG is published to the repo, so any value that could escape its attribute context (CSS injection, escapes, control bytes) aborts generation.","triggerScenarios":"render_svg() produced an element attribute containing a non-ASCII character (e.g. a translated label or owner name leaked into an attribute instead of text content), a backslash, a CSS comment sequence, or a raw control byte (newline/tab inside an attribute value). Typical root cause: editing the SVG template or exact_attribute_values/fill palettes and passing dynamic data through an attribute.","commonSituations":"Contributors localizing titles or descriptions and putting Unicode text into attributes; templating code that string-interpolates unescaped repo metadata; an editor inserting escaped characters; modifying the xkcd theme colors to values with escape sequences.","solutions":["Keep all dynamic/localized text in element text nodes (e.g. <text>...</text>), never in attributes; attributes in this pipeline are static layout values only.","If you intentionally added a new attribute value, verify it is ASCII, backslash-free, and free of /* */ and control characters, then register it in the exact_attribute_values allowlist in _validate_svg (scripts/star_history.py:1149).","Diff the two generated SVGs (static/image/star-history-light.svg / -dark.svg) against git HEAD to find which attribute changed since the last good render.","Re-run the generator with the previous history.json to confirm the failure follows your template edit, not the data."],"exampleFix":"// before: dynamic value placed in an attribute\nelement.set(\"aria-label\", f\"Stars for {owner_name}\")  // non-ASCII owner name triggers error\n\n// after: keep attributes static, put text in the node\nelement.text = f\"Stars for {owner_name}\"\nelement.set(\"role\", \"img\")  // static, ASCII, allowlisted","handlingStrategy":"validation","validationCode":"def svg_attribute_is_safe(value: str) -> bool:\n    return (\n        value.isascii()\n        and \"\\\\\" not in value\n        and \"/*\" not in value\n        and \"*/\" not in value\n        and all(ord(c) >= 0x20 for c in value)\n    )\n\n# before generating, check every attribute you plan to set\nassert all(svg_attribute_is_safe(v) for v in my_dynamic_values)","typeGuard":"def is_safe_svg_attribute_value(value: object) -> TypeGuard[str]:\n    if not isinstance(value, str):\n        return False\n    return (\n        value.isascii()\n        and \"\\\\\" not in value\n        and \"/*\" not in value\n        and \"*/\" not in value\n        and all(ord(c) >= 0x20 for c in value)\n    )","tryCatchPattern":"try:\n    _write_outputs(root, state)\nexcept StarHistoryError as exc:\n    if \"unsafe attribute value\" in str(exc):\n        # dump the offending attribute for triage, then fix the template/data\n        log.error(\"SVG attribute safety failure: %s\", exc)\n    raise","preventionTips":["Keep all dynamic or localized content in element text nodes, never attributes.","Run _validate_svg over rendered bytes in a unit test whenever the template changes.","Add a CI step that regenerates and byte-compares both SVGs so drift is caught at PR time."],"tags":["svg","security","validation","rendering"],"backgroundTag":null,"analyzedSha":"b5b53acc57189a4a42e44a23e149dc655c98fe82","analyzedAt":"2026-08-14T22:29:33.146Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}