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

medium

How insecure update manifest parsing happens in C++ UpdateHelper.cpp and how to fix it

TrafficMonitor's software update mechanism in `UpdateHelper.cpp` fetched and parsed update manifests from remote servers without validating the version string or enforcing trusted download URLs, leaving users exposed to man-in-the-middle (MITM) attacks. An attacker on the same network could intercept the update channel and inject a malicious binary under a crafted version string or an HTTP download link pointing to attacker-controlled infrastructure. The fix adds strict version-string sanitizati

high

How integer overflow in malloc happens in C bipartite matching and how to fix it

A high-severity integer overflow vulnerability was discovered in the bipartite matching algorithm implementation where unchecked multiplication operations for memory allocation could wrap around, causing undersized buffer allocations and subsequent heap overflow. The fix replaces vulnerable `malloc(sizeof(int) * V)` patterns with safe `calloc(V, sizeof(int))` calls and adds proper bounds validation to prevent exploitation.

high

How integer truncation heap overflow happens in C++ UEFI ACPI parsing and how to fix it

A high-severity integer truncation vulnerability was discovered in `Mobility.Uefi.Acpi.cpp` where heap allocation sizes were stored in a 16-bit integer (`MO_UINT16`), causing silent truncation when the computed size exceeded 65535 bytes. This led to undersized heap allocations followed by out-of-bounds writes, exploitable by an attacker who can influence ACPI SRAT table contents in virtualized environments. The fix promotes the size variable to `MO_UINTN` (platform-native width) to prevent trunc

critical

How API key exposure in configuration files happens in TOML config and how to fix it

A critical security vulnerability in `commands/webperf.toml` allowed API keys to be hardcoded directly in configuration files, creating a credential exposure risk. The documentation on line 11 suggested developers could provide `CRUX_API_KEY` or `GOOGLE_API_KEY` directly in the config, which could lead to these sensitive credentials being committed to version control or exposed in logs. The fix updated the documentation to explicitly require environment variables and warn against hardcoding cred

high

How path traversal happens in Ruby YARD server and how to fix it

A high-severity path traversal vulnerability (CVE-2026-41493) in YARD versions prior to 0.9.42 allowed attackers to read arbitrary files from servers running `yard server`. This fix upgrades the yard gem from 0.9.26 to 0.9.42 in the Gemfile and Gemfile.lock, closing a dangerous information disclosure vector that could expose configuration files, credentials, and source code.

high

How buffer overflow via sprintf() happens in C networking code and how to fix it

A high-severity buffer overflow vulnerability was discovered in `profile.c` where `sprintf()` was used to format server addresses without any bounds checking. An attacker who could influence the `SERVER_BASE_PORT` value or trigger integer overflow in the port calculation could write beyond the `server_address` buffer. The fix replaces `sprintf()` with `snprintf()` using explicit buffer size limits at both call sites (lines 99 and 220).