Back to Blog
critical SEVERITY9 min read

Heap Buffer Overflow in NanoSVG: How a Crafted SVG File Could Lead to Code Execution

A critical heap buffer overflow vulnerability was discovered and patched in the NanoSVG SVG parsing library, where missing bounds checks on memcpy operations allowed attacker-controlled SVG data to overflow heap buffers. Without validation of gradient stop counts and attribute array indices, a specially crafted SVG file could trigger arbitrary code execution. The fix adds proper bounds checking before all memory copy operations, closing a dangerous attack vector present in any application that p

O
By Orbis AppSec
β€’Published May 18, 2026β€’Reviewed June 3, 2026

Answer Summary

This is a heap buffer overflow vulnerability (CWE-122) in C's NanoSVG library where memcpy operations lacked bounds checking when parsing SVG gradient stops and attribute arrays. Attackers could supply crafted SVG files with excessive element counts to overflow heap buffers and potentially execute arbitrary code. The fix adds validation to ensure copy sizes don't exceed buffer capacities before any memcpy call, particularly when processing gradient stop counts and attribute array indices from untrusted SVG input.

Vulnerability at a Glance

cweCWE-122
fixAdd bounds checking to validate copy sizes against buffer capacities before memcpy calls
riskArbitrary code execution via malicious SVG files
languageC
root causeMissing bounds validation before memcpy operations on gradient stops and attribute arrays
vulnerabilityHeap Buffer Overflow

Heap Buffer Overflow in NanoSVG: How a Crafted SVG File Could Lead to Arbitrary Code Execution

Severity: πŸ”΄ Critical | File: 3rd/nanosvg.h | Fix: Bounds check before memcpy


Introduction

SVG files are everywhere. They power icons, illustrations, and user-uploaded graphics across millions of applications. Most developers treat them as harmless XML β€” after all, they're just vector graphics, right?

Wrong.

SVG parsers are complex pieces of software that interpret attacker-controlled data and translate it into memory structures. When that parsing logic skips a bounds check, the consequences can be catastrophic: heap corruption, memory disclosure, or full arbitrary code execution. That's exactly what was found β€” and fixed β€” in nanosvg.h, a popular single-header SVG parsing library widely embedded in C and C++ projects.

This post breaks down the vulnerability, explains how it could be exploited, and walks through the fix so you can understand not just what changed, but why it matters.


Background: What Is NanoSVG?

NanoSVG is a lightweight, single-header SVG parser written in C. Its appeal is simplicity β€” drop one header file into your project and you can parse SVG files. This convenience has made it a popular choice for game engines, GUI frameworks, and desktop applications.

The downside of that simplicity? Less code review, fewer eyes on edge cases, and a codebase that often gets vendored (copied directly into projects) and forgotten.


The Vulnerability Explained

The vulnerability is a heap buffer overflow β€” a class of memory safety bug where a program writes more data into a heap-allocated buffer than it was sized to hold. In NanoSVG's case, two distinct missing bounds checks were identified:

Issue 1: Unchecked Gradient Stop Count (Line 913)

When NanoSVG processes SVG gradient definitions, it copies gradient stop data using memcpy. The problematic pattern looks like this:

// VULNERABLE β€” no validation that nstops fits within allocated capacity
memcpy(grad->stops, stops, nstops * sizeof(NSVGgradientStop));

Here, nstops comes directly from parsing attacker-controlled SVG markup. If a crafted SVG declares more gradient stops than the destination buffer (grad->stops) was allocated to hold, the memcpy happily writes beyond the buffer's end β€” corrupting adjacent heap memory.

The allocation for grad->stops happens earlier with a fixed or estimated capacity. If the parser doesn't verify that nstops <= allocated_capacity before the copy, an attacker can supply an arbitrarily large number of stops.

Issue 2: Unbounded Attribute Stack Index (Line 766)

The second issue involves attrHead, an integer used as an array index into the attr attribute stack:

// VULNERABLE β€” attrHead not validated against array bounds
attr[attrHead].something = value;

The attr array has a fixed compile-time size. If deeply nested SVG elements push attrHead beyond that size, writes occur past the end of the array into adjacent heap memory. A crafted SVG with excessive nesting depth is all it takes.

How Could an Attacker Exploit This?

Let's walk through a realistic attack scenario:

Step 1 β€” Craft a malicious SVG

An attacker creates an SVG file containing a linear gradient with a fabricated stop count far exceeding any reasonable allocation:

<svg xmlns="http://www.w3.org/2000/svg">
  <defs>
    <linearGradient id="evil">
      <!-- Parser tricks: metadata claims N stops, actual data overflows buffer -->
      <stop offset="0%" stop-color="red"/>
      <!-- ... hundreds more stops ... -->
    </linearGradient>
  </defs>
  <rect fill="url(#evil)" width="100" height="100"/>
</svg>

Step 2 β€” Trigger the parse

The attacker delivers this file to a victim application β€” perhaps through a file upload feature, a document import dialog, or even an image preview in a file manager.

Step 3 β€” Heap corruption

The unchecked memcpy writes past the allocated buffer. Depending on what lives adjacent in the heap, this can:

  • Corrupt heap metadata (enabling heap exploitation primitives)
  • Overwrite function pointers or vtables
  • Overwrite adjacent object fields to manipulate program logic

Step 4 β€” Code execution

With careful heap manipulation (heap feng shui), an attacker can turn the overflow into controlled writes, ultimately redirecting execution to attacker-supplied shellcode or ROP chains.

Real-World Impact

Any application that:
- Accepts SVG file uploads from users
- Renders SVG files from the filesystem or network
- Embeds NanoSVG as a vendored dependency

...is potentially vulnerable to remote or local code execution simply by processing a malicious SVG file. No authentication bypass required. No special privileges needed. Just a crafted file.

This is particularly dangerous in:
- Desktop applications that open user files (the attacker sends you an SVG)
- Web services with SVG upload features
- Game engines that load SVG assets
- Document editors that import SVG graphics


The Fix

The fix is conceptually straightforward: validate before you copy.

Gradient Stop Bounds Check

Before performing the memcpy for gradient stops, the patched code verifies that nstops does not exceed the allocated capacity of grad->stops:

// FIXED β€” validate nstops against allocated capacity before copy
if (nstops > grad->nstops) {
    // Handle error: too many stops, truncate or abort
    nstops = grad->nstops;
}
memcpy(grad->stops, stops, nstops * sizeof(NSVGgradientStop));

The key insight: the number of stops to copy must be bounded by what was actually allocated. If the parsed count exceeds capacity, the code either truncates to the safe maximum or returns an error β€” but it never writes beyond the buffer.

Attribute Stack Index Check

For the attribute stack overflow, the fix validates attrHead against the array's compile-time size before using it as an index:

// FIXED β€” check attrHead before indexing into attr array
#define NSVG_MAX_ATTR 128

if (attrHead >= NSVG_MAX_ATTR) {
    // SVG nesting too deep β€” handle gracefully
    return;
}
attr[attrHead].something = value;

By checking attrHead < NSVG_MAX_ATTR before every array access, deeply nested SVG elements cause a graceful parse failure rather than a heap overflow.

Why These Fixes Work

Both fixes follow the same principle: establish an invariant before a dangerous operation and enforce it. Instead of trusting that input-derived values are safe, the code now explicitly proves they are safe before using them.

This is the essence of defensive programming in C/C++: never assume attacker-controlled values are within expected ranges.


Prevention & Best Practices

1. Never Trust Input-Derived Lengths

Any length, count, or index that originates from parsed input must be validated before use in memory operations. This is especially true for:

  • memcpy / memmove / strcpy length arguments
  • Array indices
  • Allocation sizes
// ❌ Dangerous
memcpy(dst, src, user_supplied_length);

// βœ… Safe
size_t safe_length = MIN(user_supplied_length, dst_capacity);
memcpy(dst, src, safe_length);

2. Use Safe Abstractions Where Possible

If your project allows C++, prefer std::vector, std::array, and range-checked containers over raw arrays and manual memcpy. In C, consider wrapper functions that enforce bounds:

static bool safe_memcpy(void* dst, size_t dst_size,
                         const void* src, size_t copy_size) {
    if (copy_size > dst_size) return false;
    memcpy(dst, src, copy_size);
    return true;
}

3. Enable Compiler and Runtime Protections

Modern compilers and platforms offer mitigations that make exploitation harder (though not impossible):

Protection How to Enable What It Does
AddressSanitizer -fsanitize=address Detects out-of-bounds at runtime
Stack Canaries -fstack-protector-strong Detects stack corruption
ASLR OS-level (default on modern OS) Randomizes memory layout
FORTIFY_SOURCE -D_FORTIFY_SOURCE=2 Adds bounds checks to libc functions

Enable ASan during development and testing β€” it would have caught both of these bugs immediately.

4. Fuzz Your Parsers

File parsers are prime targets for fuzzing. Tools like libFuzzer and AFL++ are excellent at finding exactly this class of bug:

# Example: fuzz an SVG parser with libFuzzer
clang -fsanitize=address,fuzzer svg_fuzz_target.c nanosvg_impl.c -o svg_fuzzer
./svg_fuzzer corpus/

A fuzzer generating random/mutated SVG inputs would likely trigger these overflows within minutes.

5. Audit Vendored Dependencies

NanoSVG is a vendored dependency β€” a copy pasted directly into the project. Vendored code is frequently forgotten after the initial copy:

  • It doesn't get updated when upstream fixes bugs
  • It doesn't appear in dependency scanners unless specifically configured
  • Developers assume "it worked before, it's fine"

Recommendations:
- Maintain an inventory of all vendored code
- Subscribe to security advisories for vendored libraries
- Consider using package managers (vcpkg, Conan, Hunter) that make updates easier
- Run static analysis tools (Coverity, CodeQL, clang-tidy) on vendored code

6. Relevant Security Standards

This vulnerability maps to well-documented weakness categories:

  • CWE-122: Heap-based Buffer Overflow
  • CWE-129: Improper Validation of Array Index
  • CWE-787: Out-of-bounds Write
  • OWASP A03:2021: Injection (includes memory injection via crafted input)

The SEI CERT C Coding Standard rules ARR38-C (guarantee that library functions do not form invalid pointers) and MEM35-C (allocate sufficient memory for an object) are directly applicable here.


Lessons for Developers

This vulnerability illustrates several patterns that appear repeatedly in security research:

πŸ”΄ Complexity hides bugs. SVG is a complex format. The more states a parser must handle, the more edge cases exist, and the more likely that some path skips a safety check.

πŸ”΄ "It's just a file" is a dangerous assumption. Files are user input. Any file format parser must treat its input as potentially adversarial.

πŸ”΄ Single-header libraries accumulate technical debt. The convenience of dropping in a .h file comes with the responsibility of maintaining it β€” including security patches.

🟒 Bounds checking is cheap. The performance cost of an integer comparison before a memcpy is negligible. The security cost of skipping it can be catastrophic.

🟒 Defense in depth helps. Even if a bounds check is missed, ASan in CI, fuzzing, and exploit mitigations like ASLR reduce the likelihood and impact of exploitation.


Conclusion

The heap buffer overflow in nanosvg.h is a textbook example of why input validation is non-negotiable in C/C++ code that processes untrusted data. Two missing bounds checks β€” one on a gradient stop count, one on an attribute array index β€” created a critical code execution vulnerability reachable simply by opening a malicious SVG file.

The fix is elegant in its simplicity: check that values derived from input are within safe bounds before using them in memory operations. That's it. Two comparisons stand between a working SVG parser and a critical exploit.

As developers, the key takeaways are:

  1. Treat all parsed input as adversarial β€” especially in C/C++
  2. Validate lengths and indices before every memory operation
  3. Fuzz your parsers β€” they will find what code review misses
  4. Keep vendored dependencies updated and audited
  5. Enable compiler sanitizers in your development and CI pipelines

Security isn't about perfection β€” it's about building layers of protection so that when one layer fails, another catches the fall. Start with the basics: validate your inputs, check your bounds, and never trust a file just because it has a .svg extension.


This vulnerability was identified and patched as part of a proactive security audit. The fix has been verified by automated scanner re-scan and LLM-assisted code review.

Have questions about memory safety or SVG parsing security? Drop a comment below or reach out to the security team.


Further Reading:
- The Art of Software Security Assessment β€” Chapter on integer and buffer overflows
- Heap Exploitation Techniques β€” Understanding how heap overflows become exploits
- libFuzzer Tutorial β€” Getting started with coverage-guided fuzzing
- NanoSVG GitHub Repository β€” Upstream source

Frequently Asked Questions

What is a heap buffer overflow?

A heap buffer overflow occurs when a program writes data beyond the allocated boundaries of a heap buffer, corrupting adjacent memory and potentially allowing attackers to execute arbitrary code or crash the application.

How do you prevent heap buffer overflow in C?

Prevent heap buffer overflow by always validating input sizes before memory operations, using bounds-checked functions like memcpy_s(), implementing size assertions, and validating array indices against known buffer limits before copying data.

What CWE is heap buffer overflow?

Heap buffer overflow is classified as CWE-122 (Heap-based Buffer Overflow), which is a child of CWE-787 (Out-of-bounds Write) and CWE-788 (Access of Memory Location After End of Buffer).

Is using malloc() with the right size enough to prevent heap buffer overflow?

No, correctly sizing malloc() is necessary but not sufficient. You must also validate that all subsequent operations (memcpy, strcpy, array indexing) respect the allocated buffer size, especially when sizes come from untrusted input.

Can static analysis detect heap buffer overflow?

Yes, static analysis tools like Coverity, CodeQL, and Clang Static Analyzer can detect many heap buffer overflow patterns, particularly when they can trace data flow from untrusted sources to unchecked memory operations.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #16

Related Articles

high

How missing Dependabot cooldown happens in GitHub Actions and how to fix it

A high-severity configuration vulnerability was discovered in a `.github/dependabot.yml` file that lacked a cooldown period for package updates. Without this safeguard, Dependabot could immediately propose updates to newly published package versionsβ€”including potentially malicious or unstable releases. The fix adds a simple `cooldown` block with a 7-day waiting period before any new package version is suggested.

high

How Server-Sent Events Injection via Unsanitized Newlines happens in Node.js h3 and how to fix it

A high-severity Server-Sent Events (SSE) injection vulnerability (CVE-2026-33128) was discovered in the h3 HTTP framework, where unsanitized newline characters in event stream fields could allow attackers to inject arbitrary SSE messages. The fix upgrades h3 from version 1.15.5 to 1.15.6 in the frontend's dependency tree, ensuring that newline characters are properly sanitized before being written to event streams.

high

How Memory Exhaustion via Large Comma-Separated Selector Lists happens in Python Soup Sieve and how to fix it

A high-severity memory exhaustion vulnerability (CVE-2026-49476) was discovered in Soup Sieve version 2.8.3, affecting Python applications that parse CSS selectors from user-controlled input. The vulnerability allows attackers to craft malicious selector lists that consume excessive memory, potentially causing denial of service. The fix involves upgrading to soupsieve 2.8.4, which implements proper resource limits on selector parsing.

high

How prototype pollution via `__proto__` key happens in Node.js defu and how to fix it

A high-severity prototype pollution vulnerability (CVE-2026-35209) was discovered in the `defu` package version 6.1.4, which allowed attackers to inject properties into JavaScript's `Object.prototype` via the `__proto__` key in defaults arguments. The fix upgrades `defu` to version 6.1.5 in the frontend's dependency tree, protecting downstream consumers like `c12` and `dotenv` configuration loaders from malicious property injection.

critical

How buffer overflow in memcpy() happens in Node.js N-API bindings and how to fix it

A critical buffer overflow vulnerability was discovered in the GetBufferAsVector() function in examples_nodejs/src/zupt_napi.cpp, where memcpy() copied data from JavaScript Uint8Array buffers without proper bounds validation. This vulnerability could allow attackers to trigger memory corruption by providing maliciously crafted input arrays to the native Node.js module, potentially leading to crashes or arbitrary code execution.

high

How memory exhaustion via large comma-separated selector lists happens in Python soupsieve and how to fix it

A high-severity memory exhaustion vulnerability (CVE-2026-49476) was discovered in soupsieve 2.8.3, a CSS selector library used by BeautifulSoup in Python. An attacker who could influence CSS selector input could craft large comma-separated selector lists to exhaust system memory, causing denial of service. The fix upgrades soupsieve from 2.8.3 to 2.8.4 in the backend's `uv.lock` dependency file.