{"record":{"id":"5dfff3b6a85fd107","repo":"666ghj/MiroFish","slug":"generated-svg-contains-an-external-resource","errorCode":null,"errorMessage":"generated SVG contains an external resource","messagePattern":"generated SVG contains an external resource","errorType":"exception","errorClass":"StarHistoryError","httpStatus":null,"severity":"critical","filePath":"scripts/star_history.py","lineNumber":1239,"sourceCode":"            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:\n        raise StarHistoryError(\"generated SVG must contain both reviewed images\")\n    try:\n        avatar = base64.b64decode(OWNER_AVATAR_BASE64, validate=True)\n    except ValueError as exc:\n        raise StarHistoryError(\"reviewed avatar data is invalid\") from exc\n    if (\n        len(avatar) > MAX_INLINE_AVATAR_BYTES\n        or hashlib.sha256(avatar).hexdigest() != OWNER_AVATAR_SHA256\n        or _reviewed_avatar_dimensions(avatar) != OWNER_AVATAR_DIMENSIONS","sourceCodeStart":1221,"sourceCodeEnd":1257,"githubUrl":"https://github.com/666ghj/MiroFish/blob/b5b53acc57189a4a42e44a23e149dc655c98fe82/scripts/star_history.py#L1221-L1257","documentation":"Raised by _validate_svg when an href attribute is present but the element is not an <image>, or its value is not one of the two pinned data URIs (OWNER_AVATAR_DATA_URI / WATERMARK_LOGO_DATA_URI). The published SVG must be fully self-contained: no external fetches, and the only embedded images are the byte-reviewed avatar and watermark.","triggerScenarios":"Adding any href (e.g. on <a>, <use>, <tspan>) other than on the two blessed <image> elements; changing the avatar/watermark image without regenerating the pinned DATA_URI constants; a template edit that swaps href for xlink:href-style external URLs; embedding a different logo PNG.","commonSituations":"Rebranding: someone replaces static assets or the owner avatar but only edits the SVG template, not OWNER_AVATAR_BASE64/SHA256 constants; adding a clickable link (<a href=...>) around the chart; pulling in an icon set that references external sprites.","solutions":["Locate every href in the generated SVG (grep 'href=' static/image/star-history-*.svg) and confirm only the two <image> elements carry it.","If replacing the avatar or watermark image, update the whole constant set together: OWNER_AVATAR_BASE64, OWNER_AVATAR_SHA256, OWNER_AVATAR_DIMENSIONS (or WATERMARK_LOGO_*), since the data URI is derived from the base64 constant.","Remove <a>/external-link wrappers; this design intentionally forbids links inside the published SVG.","If you need a new embedded image, extend the avatar/watermark pattern: pinned data URI + count check + hash check, never a bare allowlist entry."],"exampleFix":"# before: external link added to the chart\n<a href=\"https://github.com/owner/repo\">...</a>\n\n# after: keep the SVG link-free; put links in the README that embeds the SVG\n# (no href elements other than the two reviewed <image> data URIs)","handlingStrategy":"validation","validationCode":"def only_reviewed_hrefs(root) -> bool:\n    for el in root.iter():\n        href = el.get(\"href\")\n        if href is not None:\n            if not el.tag.endswith(\"}image\"):\n                return False\n            if href not in {OWNER_AVATAR_DATA_URI, WATERMARK_LOGO_DATA_URI}:\n                return False\n    return True","typeGuard":null,"tryCatchPattern":"try:\n    _validate_svg(svg_bytes)\nexcept StarHistoryError as exc:\n    if \"external resource\" in str(exc):\n        # grep the SVG for href=/xlink:href and confirm only the two data URIs remain\n        ...","preventionTips":["Embed images as pinned base64 data URIs; never reference URLs in the SVG.","When swapping avatar/watermark assets, regenerate the DATA_URI constants from the same bytes as the SHA-256 pins.","Exclude the generated SVGs from optimizers that externalize assets."],"tags":["svg","security","external-resource","assets"],"backgroundTag":null,"analyzedSha":"b5b53acc57189a4a42e44a23e149dc655c98fe82","analyzedAt":"2026-08-14T22:29:33.146Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}