Stop untrusted preallocation during string deserialization

This is an easy memory denial of service attack.
This commit is contained in:
teor 2021-03-19 17:18:46 +10:00 committed by Deirdre Connolly
parent db2f920d96
commit 609d70ae53
5 changed files with 47 additions and 28 deletions

View File

@ -21,6 +21,7 @@ pub use header::BlockTimeError;
pub use header::{CountedHeader, Header};
pub use height::Height;
pub use root_hash::RootHash;
pub use serialize::MAX_BLOCK_BYTES;
use serde::{Deserialize, Serialize};

View File

@ -11,6 +11,15 @@ use super::SerializationError;
pub trait ReadZcashExt: io::Read {
/// Reads a `u64` using the Bitcoin `CompactSize` encoding.
///
/// # Security
///
/// Deserialized sizes must be validated before being used.
///
/// Preallocating vectors using untrusted `CompactSize`s allows memory
/// denial of service attacks. Valid sizes must be less than
/// `MAX_BLOCK_BYTES / min_serialized_item_bytes` (or a lower limit
/// specified by the Zcash consensus rules or Bitcoin network protocol).
///
/// # Examples
///
/// ```rust
@ -87,15 +96,6 @@ pub trait ReadZcashExt: io::Read {
Ok(SocketAddr::new(ip_addr, port))
}
/// Read a Bitcoin-encoded UTF-8 string.
#[inline]
fn read_string(&mut self) -> Result<String, SerializationError> {
let len = self.read_compactsize()?;
let mut buf = vec![0; len as usize];
self.read_exact(&mut buf)?;
String::from_utf8(buf).map_err(|_| SerializationError::Parse("invalid utf-8"))
}
/// Convenience method to read a `[u8; 4]`.
#[inline]
fn read_4_bytes(&mut self) -> io::Result<[u8; 4]> {

View File

@ -1,6 +1,7 @@
use std::io;
use super::{ReadZcashExt, SerializationError};
use byteorder::ReadBytesExt;
/// Consensus-critical serialization for Zcash.
///
@ -33,6 +34,21 @@ impl<T: ZcashDeserialize> ZcashDeserialize for Vec<T> {
}
}
/// Read a byte.
impl ZcashDeserialize for u8 {
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
Ok(reader.read_u8()?)
}
}
/// Read a Bitcoin-encoded UTF-8 string.
impl ZcashDeserialize for String {
fn zcash_deserialize<R: io::Read>(reader: R) -> Result<Self, SerializationError> {
let bytes: Vec<_> = Vec::zcash_deserialize(reader)?;
String::from_utf8(bytes).map_err(|_| SerializationError::Parse("invalid utf-8"))
}
}
/// Helper for deserializing more succinctly via type inference
pub trait ZcashDeserializeInto {
/// Deserialize based on type inference

View File

@ -44,25 +44,27 @@ impl<P: ZkSnarkProof> ZcashSerialize for JoinSplitData<P> {
impl<P: ZkSnarkProof> ZcashDeserialize for Option<JoinSplitData<P>> {
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
let joinsplits: Vec<sprout::JoinSplit<P>> = Vec::zcash_deserialize(&mut reader)?;
if joinsplits.is_empty() {
Ok(None)
} else {
let (first, rest) = joinsplits
.split_first()
.expect("a non-empty Vec must have at least one entry");
let num_joinsplits = reader.read_compactsize()?;
match num_joinsplits {
0 => Ok(None),
n => {
let first = sprout::JoinSplit::zcash_deserialize(&mut reader)?;
let mut rest = Vec::with_capacity((n - 1) as usize);
for _ in 0..(n - 1) {
rest.push(sprout::JoinSplit::zcash_deserialize(&mut reader)?);
}
let pub_key = reader.read_32_bytes()?.into();
let sig = reader.read_64_bytes()?.into();
Ok(Some(JoinSplitData {
first: first.clone(),
rest: rest.to_vec(),
first,
rest,
pub_key,
sig,
}))
}
}
}
}
impl ZcashSerialize for Transaction {
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {

View File

@ -464,7 +464,7 @@ impl Codec {
reader.read_socket_addr()?,
),
nonce: Nonce(reader.read_u64::<LittleEndian>()?),
user_agent: reader.read_string()?,
user_agent: String::zcash_deserialize(&mut reader)?,
start_height: block::Height(reader.read_u32::<LittleEndian>()?),
relay: match reader.read_u8()? {
0 => false,
@ -488,7 +488,7 @@ impl Codec {
fn read_reject<R: Read>(&self, mut reader: R) -> Result<Message, Error> {
Ok(Message::Reject {
message: reader.read_string()?,
message: String::zcash_deserialize(&mut reader)?,
ccode: match reader.read_u8()? {
0x01 => RejectReason::Malformed,
0x10 => RejectReason::Invalid,
@ -501,7 +501,7 @@ impl Codec {
0x50 => RejectReason::Other,
_ => return Err(Error::Parse("invalid RejectReason value in ccode field")),
},
reason: reader.read_string()?,
reason: String::zcash_deserialize(&mut reader)?,
// Sometimes there's data, sometimes there isn't. There's no length
// field, this is just implicitly encoded by the body_len.
// Apparently all existing implementations only supply 32 bytes of