Back to Blog
critical SEVERITY8 min read

Stack Buffer Overflow in CSS Selector Parsing: A Critical C Vulnerability Fixed

A critical stack buffer overflow vulnerability was discovered and patched in `lib/css/src/selector.c`, where unbounded `strcpy()` calls could allow attackers to overwrite stack memory and achieve arbitrary code execution. This fix eliminates a classic but dangerous class of memory corruption bug that has plagued C codebases for decades. Understanding how this vulnerability works β€” and how it was fixed β€” is essential knowledge for any developer working with low-level C code or parsing user-contro

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

Answer Summary

A stack buffer overflow (CWE-121) was found in C code within `lib/css/src/selector.c`, where unbounded `strcpy()` calls copied user-controlled CSS selector strings into fixed-size stack buffers without length validation. The fix replaces `strcpy()` with `strncpy()` using explicit buffer size limits, preventing attackers from overwriting adjacent stack memory to hijack control flow or execute arbitrary code.

Vulnerability at a Glance

cweCWE-121
fixReplace strcpy() with strncpy() using explicit buffer size bounds and null-termination
riskArbitrary code execution via crafted CSS selector input
languageC
root causeUnbounded strcpy() copying user-controlled CSS selector strings into fixed-size stack buffers
vulnerabilityStack Buffer Overflow

Stack Buffer Overflow in CSS Selector Parsing: A Critical C Vulnerability Fixed

Severity: πŸ”΄ Critical | CWE: CWE-120 (Buffer Copy Without Checking Size of Input) | File: lib/css/src/selector.c


Introduction

Buffer overflows are one of the oldest and most dangerous vulnerability classes in software security. Despite being well-understood for decades, they continue to appear in production codebases β€” and when they do, the consequences can be severe. A recently patched vulnerability in a CSS selector parsing library serves as a timely reminder of why strcpy() is considered one of C's most dangerous standard library functions.

This post breaks down the vulnerability, explains how it could be exploited, and walks through the fix β€” giving you the knowledge to recognize and prevent this class of bug in your own code.


The Vulnerability Explained

What Happened?

Inside lib/css/src/selector.c, a function was responsible for constructing a "full name" string for a CSS selector node. It did this by concatenating several fields β€” type, id, classes, and status β€” into a single fixed-size stack buffer using multiple calls to strcpy() and strcat().

Here's a simplified representation of what the vulnerable code looked like:

// VULNERABLE CODE (illustrative example)
void build_selector_fullname(SelectorNode *node) {
    char fullname[256];  // Fixed-size stack buffer

    strcpy(fullname, node->type);    // No bounds check
    strcat(fullname, node->id);      // No bounds check
    strcat(fullname, node->classes); // No bounds check
    strcat(fullname, node->status);  // No bounds check

    // Use fullname for further processing...
}

The critical problem: there is no bounds checking on any of these copy operations. The buffer fullname is allocated with a fixed size (e.g., 256 bytes) on the stack. If the combined length of type + id + classes + status exceeds that size, the function happily writes past the end of the buffer β€” directly into adjacent stack memory.

Why Is This So Dangerous?

When data is written beyond the bounds of a stack-allocated buffer, it can overwrite:

  • The saved return address β€” the address the CPU jumps to when the function returns
  • Saved frame pointers β€” used by the debugger and calling convention
  • Local variables of the calling function β€” potentially corrupting program logic
  • Stack canary values β€” security mitigations designed to detect this exact attack

By carefully crafting input that overflows the buffer with a specific payload, an attacker can redirect execution to arbitrary code β€” a technique known as return-oriented programming (ROP) or classic stack smashing.

How Could It Be Exploited?

Consider the attack scenario:

  1. An attacker crafts a malicious CSS stylesheet with an extremely long selector β€” for example, a class name that is 500 characters long.
  2. The application parses this stylesheet and passes the selector fields to build_selector_fullname().
  3. The strcpy/strcat calls write 500+ bytes into a 256-byte buffer.
  4. The excess bytes overwrite the return address on the stack.
  5. When the function returns, the CPU jumps to attacker-controlled code.
  6. The attacker achieves arbitrary code execution in the context of the running process.

This is classified under CWE-120: Buffer Copy Without Checking Size of Input ('Classic Buffer Overflow'), and it's rated Critical because successful exploitation can lead to full system compromise.

Real-World Impact

  • Arbitrary code execution on the host system
  • Privilege escalation if the process runs with elevated permissions
  • Data exfiltration β€” reading sensitive memory contents
  • Denial of service β€” crashing the application by corrupting the stack
  • Supply chain risk β€” if this library is embedded in other software, all downstream consumers are affected

The Fix

What Changed?

The fix replaces the unbounded strcpy()/strcat() calls with their length-aware, bounds-checking counterparts: strncpy() and strncat() β€” or ideally, a safer pattern using snprintf(), which provides explicit size control and null-termination guarantees in a single call.

Here's what a safe version of this function looks like:

// SAFE CODE (illustrative example of the fix)
void build_selector_fullname(SelectorNode *node) {
    char fullname[256];
    size_t remaining = sizeof(fullname) - 1;

    fullname[0] = '\0'; // Ensure null-terminated start

    strncat(fullname, node->type,    remaining);
    remaining -= strlen(node->type) < remaining ? strlen(node->type) : remaining;

    strncat(fullname, node->id,      remaining);
    remaining -= strlen(node->id) < remaining ? strlen(node->id) : remaining;

    strncat(fullname, node->classes, remaining);
    remaining -= strlen(node->classes) < remaining ? strlen(node->classes) : remaining;

    strncat(fullname, node->status,  remaining);
}

Or, even cleaner and less error-prone using snprintf():

// PREFERRED SAFE PATTERN using snprintf()
void build_selector_fullname(SelectorNode *node) {
    char fullname[256];

    int written = snprintf(
        fullname,
        sizeof(fullname),
        "%s%s%s%s",
        node->type,
        node->id,
        node->classes,
        node->status
    );

    if (written < 0 || (size_t)written >= sizeof(fullname)) {
        // Handle truncation or error β€” log, return early, or abort
        handle_error("Selector fullname truncated or encoding error");
        return;
    }

    // Safely use fullname...
}

Why snprintf() Is the Right Tool Here

Function Bounds Checking Null Termination Error Detection
strcpy() ❌ None βœ… Yes ❌ None
strcat() ❌ None βœ… Yes ❌ None
strncpy() βœ… Yes ⚠️ Not guaranteed ❌ Limited
strncat() βœ… Yes βœ… Yes ❌ Limited
snprintf() βœ… Yes βœ… Yes βœ… Returns length

snprintf() is preferred because:
- It enforces a hard maximum output size including the null terminator
- It returns the number of bytes that would have been written, letting you detect truncation
- It handles the entire concatenation in one readable call, reducing the chance of off-by-one errors


Prevention & Best Practices

1. Never Use strcpy() or strcat() on Untrusted Input

These functions have no concept of buffer size. Treat them as deprecated for any code that handles external data. Many organizations enforce this through static analysis rules.

// ❌ NEVER do this with external input
strcpy(dest, user_controlled_input);

// βœ… Always do this
snprintf(dest, sizeof(dest), "%s", user_controlled_input);

2. Validate Input Length Before Processing

Before passing user-controlled strings into any buffer operation, validate their length:

if (strlen(node->type) + strlen(node->id) + strlen(node->classes) + strlen(node->status) >= sizeof(fullname)) {
    return ERROR_INPUT_TOO_LONG;
}

3. Use Compiler Hardening Flags

Modern compilers offer several protections against buffer overflows:

# Enable stack canaries
gcc -fstack-protector-strong

# Enable address space layout randomization support
gcc -fpie -pie

# Enable overflow detection (adds runtime checks)
gcc -D_FORTIFY_SOURCE=2 -O2

# Enable all warnings
gcc -Wall -Wextra -Werror

4. Use Static Analysis Tools

Integrate static analysis into your CI/CD pipeline to catch these issues before they reach production:

Flawfinder, for example, would have flagged this code immediately:

[lib/css/src/selector.c:84] (error) strcpy() called with destination buffer
of insufficient size β€” potential buffer overflow (CWE-120)

5. Consider Memory-Safe Alternatives

For new projects or major refactors, consider languages or libraries that eliminate this class of bug entirely:

  • Rust β€” ownership model prevents buffer overflows at compile time
  • C++ with std::string β€” automatic memory management
  • Safe C libraries β€” like SafeStr or Microsoft's strsafe.h

6. Fuzz Test Your Parsers

Parsers are a prime target for buffer overflow attacks because they process external, attacker-controlled data. Use fuzzing to automatically generate edge-case inputs:

# Using AFL++ to fuzz a CSS parser
afl-fuzz -i input_corpus/ -o findings/ -- ./css_parser @@

Fuzzing would likely have discovered this vulnerability by generating extremely long selector strings.

Security Standards Reference


Conclusion

This vulnerability is a textbook example of why strcpy() earned its reputation as one of C's most dangerous functions. A few lines of code, written without size awareness, created a critical security hole capable of enabling arbitrary code execution.

The key takeaways from this fix:

  1. strcpy() and strcat() are unsafe for any input you don't fully control β€” replace them with snprintf() or strncat() with explicit size limits.
  2. Fixed-size stack buffers + unbounded copies = stack overflow β€” always calculate the maximum possible input size before choosing a buffer size.
  3. Parsers deserve extra scrutiny β€” they process external data by definition, making them high-value targets.
  4. Defense in depth matters β€” compiler hardening, static analysis, and fuzzing work together to catch what code review misses.

Buffer overflows have been on the OWASP Top 10 and in security advisories since the 1980s. They persist because C gives developers enormous power with very little guardrails. The antidote is discipline, tooling, and a healthy respect for the damage a few unchecked bytes can do.

Write bounds-aware code. Fuzz your parsers. Trust no input.


This vulnerability was identified and patched as part of an automated security review process. Security fixes like this one are a normal, healthy part of software development β€” what matters is catching them early and learning from them.

Found a security issue in your codebase? Consider integrating automated security scanning into your CI/CD pipeline to catch vulnerabilities before they reach production.

Frequently Asked Questions

What is a stack buffer overflow?

A stack buffer overflow occurs when a program writes more data to a stack-allocated buffer than it can hold, overwriting adjacent memory including return addresses, which can allow attackers to redirect program execution.

How do you prevent stack buffer overflow in C?

Use bounded string functions like strncpy(), snprintf(), or strlcpy() instead of unbounded functions like strcpy() and sprintf(). Always validate input length before copying and enforce explicit buffer size limits.

What CWE is stack buffer overflow?

CWE-121 (Stack-based Buffer Overflow), which is a child of CWE-787 (Out-of-bounds Write) and CWE-120 (Buffer Copy without Checking Size of Input).

Is using strncpy() enough to prevent stack buffer overflow?

strncpy() with a proper size limit prevents the overflow itself, but you must also ensure null-termination of the destination buffer since strncpy() does not guarantee it when the source exceeds the limit. Using strlcpy() where available is even safer.

Can static analysis detect stack buffer overflow?

Yes, static analysis tools like Coverity, PVS-Studio, and compiler warnings (-Wstringop-overflow) can detect many instances of unbounded strcpy() into fixed-size buffers. Tools like Semgrep can also flag dangerous patterns.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #323

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.