mirror of https://github.com/fafhrd91/actix-net
fix(codec): add line length limit to `LineCodec` (#817)
This commit is contained in:
parent
964efc3c80
commit
c5c8b2ef49
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
- Added `LinesCodec::new_with_max_length` to enforce a maximum line length and mitigate potential
|
||||||
|
unbounded-buffer DoS when handling untrusted input.
|
||||||
- Minimum supported Rust version (MSRV) is now 1.88.
|
- Minimum supported Rust version (MSRV) is now 1.88.
|
||||||
|
|
||||||
## 0.5.2
|
## 0.5.2
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,51 @@ use super::{Decoder, Encoder};
|
||||||
///
|
///
|
||||||
/// Will split input up by LF or CRLF delimiters. Carriage return characters at the end of lines are
|
/// Will split input up by LF or CRLF delimiters. Carriage return characters at the end of lines are
|
||||||
/// not preserved.
|
/// not preserved.
|
||||||
#[derive(Debug, Copy, Clone, Default)]
|
///
|
||||||
|
/// # Security
|
||||||
|
///
|
||||||
|
/// When used with untrusted input, it is recommended to set a maximum line length with
|
||||||
|
/// [`LinesCodec::new_with_max_length`]. Without a length limit, the internal read buffer can grow
|
||||||
|
/// without bound if a peer sends an unbounded amount of data without a `\n`, potentially leading
|
||||||
|
/// to memory exhaustion (DoS).
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub struct LinesCodec;
|
pub struct LinesCodec {
|
||||||
|
max_length: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LinesCodec {
|
||||||
|
/// Creates a new `LinesCodec` with no maximum line length.
|
||||||
|
///
|
||||||
|
/// Consider using [`LinesCodec::new_with_max_length`] when working with untrusted input.
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
max_length: usize::MAX,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new `LinesCodec` with a maximum line length, in bytes.
|
||||||
|
///
|
||||||
|
/// The limit applies to the bytes before the line delimiter (`\n`). If present, a trailing
|
||||||
|
/// carriage return (`\r`) in `\r\n` sequences is not counted towards the limit.
|
||||||
|
///
|
||||||
|
/// Using a length limit is recommended when working with untrusted input to avoid unbounded
|
||||||
|
/// buffering.
|
||||||
|
pub const fn new_with_max_length(max_length: usize) -> Self {
|
||||||
|
Self { max_length }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the maximum permitted line length, in bytes.
|
||||||
|
pub const fn max_length(&self) -> usize {
|
||||||
|
self.max_length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LinesCodec {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T: AsRef<str>> Encoder<T> for LinesCodec {
|
impl<T: AsRef<str>> Encoder<T> for LinesCodec {
|
||||||
type Error = io::Error;
|
type Error = io::Error;
|
||||||
|
|
@ -38,10 +80,33 @@ impl Decoder for LinesCodec {
|
||||||
let len = match memchr(b'\n', src) {
|
let len = match memchr(b'\n', src) {
|
||||||
Some(n) => n,
|
Some(n) => n,
|
||||||
None => {
|
None => {
|
||||||
|
// No delimiter yet; if current buffered data already exceeds the maximum line
|
||||||
|
// length, abort to avoid unbounded memory growth.
|
||||||
|
let max = self.max_length;
|
||||||
|
let max_cr = max.saturating_add(1);
|
||||||
|
|
||||||
|
if src.len() > max && !(src.len() == max_cr && src.last() == Some(&b'\r')) {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"max line length exceeded",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Reject overly long lines before splitting/advancing buffers.
|
||||||
|
let max = self.max_length;
|
||||||
|
let max_cr = max.saturating_add(1);
|
||||||
|
|
||||||
|
if len > max && !(len == max_cr && src.get(len - 1) == Some(&b'\r')) {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"max line length exceeded",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
// split up to new line char
|
// split up to new line char
|
||||||
let mut buf = src.split_to(len);
|
let mut buf = src.split_to(len);
|
||||||
debug_assert_eq!(len, buf.len());
|
debug_assert_eq!(len, buf.len());
|
||||||
|
|
@ -155,4 +220,13 @@ mod tests {
|
||||||
codec.encode("1234567891112131", &mut buf).unwrap();
|
codec.encode("1234567891112131", &mut buf).unwrap();
|
||||||
assert_eq!(&buf[..], b"1234567891112131\n");
|
assert_eq!(&buf[..], b"1234567891112131\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lines_decoder_errors_on_overlong_line_without_delimiter() {
|
||||||
|
let mut codec = LinesCodec::new_with_max_length(4);
|
||||||
|
let mut buf = BytesMut::from(&b"aaaaa"[..]);
|
||||||
|
|
||||||
|
let err = codec.decode(&mut buf).unwrap_err();
|
||||||
|
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue