Jan 29, 2026·6 min read·33 visits
The Cap'n Proto Rust crate exposed public fields in `constant::Reader` and `StructSchema` that allowed safe code to inject arbitrary data. This data was subsequently processed by internal `unchecked` functions, leading to Undefined Behavior (UB).
A critical undefined behavior vulnerability in the Rust implementation of Cap'n Proto allows safe code to trigger memory corruption. By exposing internal raw pointers and unchecked offsets through public struct fields, the library violated Rust's safety guarantees, turning malformed schema constants into potential remote code execution or denial of service vectors.
Cap'n Proto markets itself as "infinity times faster" than Protocol Buffers because it doesn't just serialize data; it lays it out in memory exactly as it will be sent over the wire. Zero encoding, zero decoding. It's a raw memory mapping dream. In C++, this is business as usual—you manage the pointers, you take the risk. But this is the Rust implementation we're talking about.
The promise of Rust is simple: if you don't write the word unsafe, you shouldn't be able to segfault. The compiler is the bouncer, and it checks everyone's ID. But in version 0.23 and below, the capnproto crate left the back door unlocked. It allowed "Safe" Rust code to construct objects that violated internal invariants, handing them off to performance-critical code that assumed everything was kosher. The result? A perfectly valid-looking Rust program that can corrupt memory, read out of bounds, or crash spectacularly.
The vulnerability boils down to a classic API design blunder: exposing raw internals that the library logic implicitly trusts. In Cap'n Proto, performance is king, so internal functions often use unchecked operations to skip bounds checks. For example, PointerReader::get_root_unchecked assumes that the pointer logic it's about to perform is valid because it relies on the data having been validated during creation.
The problem? The structs holding that data—specifically constant::Reader and RawStructSchema—had their fields marked as pub. This meant any downstream developer (or attacker writing a plugin) could manually instantiate these structs. You didn't need a valid Cap'n Proto schema; you just needed to fill a struct with garbage data.
Because the fields were public, the library had no way to enforce invariants at construction time. When the user later called a safe method like .get(), the library took that user-supplied garbage, assumed it was a valid memory map, and passed it to get_root_unchecked. It’s like handing a loaded gun to a toddler because you assumed only a trained soldier would ever walk into the armory.
Let's look at the code that allowed this mutiny. In src/constant.rs, the Reader struct was defined like this:
// The Vulnerable Definition
pub struct Reader<T> {
#[doc(hidden)]
pub phantom: PhantomData<T>,
#[doc(hidden)]
pub words: &'static [crate::Word],
}See that pub words? That is the root of all evil here. Even though it is hidden from documentation, it is accessible to code. A user can create a Reader with a slice of words pointing to 0xDEADBEEF or random offsets.
The fix in version 0.24.0 is a lesson in Rust encapsulation. The developers locked down the visibility and forced the use of an unsafe constructor, shifting the burden of safety back to the developer:
// The Fixed Definition
pub struct Reader<T> {
pub(crate) phantom: PhantomData<T>,
pub(crate) words: &'static [crate::Word], // No longer public!
}
impl<T> Reader<T> {
// You want to build this manually? You sign the waiver.
pub const unsafe fn new(words: &'static [crate::Word]) -> Self {
Self {
phantom: PhantomData,
words,
}
}
}By marking the constructor unsafe, the library now complies with Rust's safety contract. If you pass bad data and it segfaults, it's your fault, not the library's.
Exploiting this doesn't require complex heap feng shui or ROP chains; it just requires writing "Safe" Rust that lies. An attacker works within the bounds of the language syntax but violates the semantic expectations of the library.
Here is a conceptual Proof of Concept showing how to trigger Undefined Behavior without using a single unsafe block in the attacking code (prior to the patch):
use capnp::constant;
use core::marker::PhantomData;
fn main() {
// 1. Create a slice of 'words' that represent a garbage pointer.
// In Cap'n Proto, specific bit patterns represent offsets.
// We fake a pointer that points way out of bounds.
let malicious_words = &[capnp::word(0xFFFFFFFF, 0xFFFFFFFF)];
// 2. Manually construct the Reader using the public fields.
// The compiler allows this because 'words' was public.
let reader: constant::Reader<some_type::Owned> = constant::Reader {
phantom: PhantomData,
words: malicious_words,
};
// 3. Trigger the UB.
// .get() calls internal unchecked functions.
// The library tries to follow the 0xFFFFFFFF offset and reads invalid memory.
let _ = reader.get();
}When reader.get() executes, the application will likely crash (SIGSEGV) or, worse, read sensitive memory adjacent to the buffer if the offset is carefully calculated. In a scenario where schemas are dynamically loaded or defined by untrusted plugins, this allows a complete bypass of memory safety.
In the Rust ecosystem, a vulnerability labeled "Undefined Behavior in Safe Code" is the highest tier of defect. It erodes the trust model of the entire dependency tree. If capnproto is used in a network service (its primary use case), and that service processes user-supplied schema definitions or constants, an attacker can crash the node trivially.
While this specific vector requires the ability to construct the Reader or Schema object (which is often done at compile time via capnpc), systems that use dynamic schema loading or plugin architectures are vulnerable. The impact ranges from Denial of Service (DoS) via panic/segfault to potential information disclosure if the out-of-bounds read returns valid memory regions to the attacker.
Remediation is straightforward but requires code changes. Upgrade to capnproto version 0.24.0 immediately. If you were manually constructing these structs (which you shouldn't have been doing, you naughty developer), your build will break.
You will need to switch to using the generated code from capnpc or, if you absolutely must create these manually, wrap your creation logic in an unsafe block and use the new Reader::new() constructor. This explicitly acknowledges that you have verified the input data.
For security teams auditing Rust codebases: Grep for direct struct initialization of capnp::constant::Reader or capnp::introspect::RawStructSchema. If you see them being built with curly braces { ... } rather than a constructor method, you are looking at vulnerable code.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
capnproto (Rust) Cap'n Proto | < 0.24.0 | 0.24.0 |
| Attribute | Detail |
|---|---|
| Attack Vector | Local / Context-dependent |
| Impact | Memory Corruption / Undefined Behavior |
| CWEs | CWE-242, CWE-822, CWE-119 |
| Status | Patched in 0.24.0 |
| Language | Rust |
| Bug Class | Unsound API / Safe Wrapper around Unsafe |
The application allows the use of inherently dangerous functions by exposing internal memory structures to safe code, leading to untrusted pointer dereferences.
An incorrect authorization vulnerability (CWE-863) in Snipe-IT versions prior to 8.6.0 allows authenticated, low-privileged users with granular 'users.edit' permissions to modify restricted user flags ('activated' and 'ldap_import') and merge high-privileged administrator accounts into standard user accounts. This allows an attacker to lock administrators out of the system or completely hijack administrator accounts.
An open redirect vulnerability exists in Flask-Security versions up to and including 5.8.0. This flaw allows remote, unauthenticated attackers to perform open redirects by exploiting a parser differential between Python's standard library urlsplit() function and modern web browsers when subdomain redirection is allowed.
An incomplete security patch for CVE-2026-24421 in phpMyFAQ allows authenticated low-privileged users to bypass role-based access controls. While the initial patch addressed missing authorization in the BackupController, it left four critical write-enabled endpoints vulnerable. This allows remote attackers with a valid low-privilege API token to perform unauthorized data modifications, creating categories, creating FAQs, updating FAQs, and injecting questions directly into the database.
An in-depth security audit of the skillctl command-line package manager revealed five critical and high-severity security vulnerabilities. The identified flaws span parameter-level command argument injection via the source_sha parameter, uncontrolled resource consumption (Denial of Service) through unnamed UNIX FIFOs and character devices, directory path traversal in the destination argument, commit-message trailer forgery via newline injection in skill names, and local credential exfiltration leveraging UNIX hardlinks. These vulnerabilities represent significant vectors for workstation compromise when executing agentic tasks in repositories containing untrusted files or pull requests. Remediation was introduced in version v0.1.3.
CVE-2026-48153 is a Server-Side Request Forgery (SSRF) vulnerability in the Budibase OAuth2 SDK prior to version 3.39.0. It allows authenticated low-privileged users to bypass outbound network security blacklists and send arbitrary requests to internal subnets or cloud metadata services.
The self-hosted Slack Nebula VPN control plane, nebula-mesh, stored high-privilege enrollment tokens in plaintext inside its SQLite database. This flaw allowed any adversary with read access to the database to retrieve pending tokens and enroll unauthorized hosts into the secure VPN mesh.