diff options
author | Antoine A <> | 2021-12-09 16:52:55 +0100 |
---|---|---|
committer | Antoine A <> | 2021-12-09 16:52:55 +0100 |
commit | f9fe9248934080e57b5359dab4b53d0b1852f9b5 (patch) | |
tree | febc6003668317b31425d3d90b8a5cce0d8995c5 | |
parent | b4876e58b2d95e3745978717ed6efa763b054d08 (diff) | |
download | depolymerization-f9fe9248934080e57b5359dab4b53d0b1852f9b5.tar.gz depolymerization-f9fe9248934080e57b5359dab4b53d0b1852f9b5.tar.bz2 depolymerization-f9fe9248934080e57b5359dab4b53d0b1852f9b5.zip |
Improve resilience against malicious request
-rw-r--r-- | Cargo.lock | 41 | ||||
-rw-r--r-- | script/test_gateway.sh | 26 | ||||
-rw-r--r-- | wire-gateway/Cargo.toml | 16 | ||||
-rw-r--r-- | wire-gateway/src/json.rs | 65 | ||||
-rw-r--r-- | wire-gateway/src/main.rs | 16 |
5 files changed, 78 insertions, 86 deletions
@@ -47,19 +47,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38de00daab4eac7d753e97697066238d67ce9d7e2d823ab4f72fe14af29f3f33" [[package]] -name = "async-compression" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443ccbb270374a2b1055fc72da40e1f237809cd6bb0e97e66d264cd138473a6" -dependencies = [ - "flate2", - "futures-core", - "memchr", - "pin-project-lite", - "tokio", -] - -[[package]] name = "async-trait" version = "0.1.51" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -265,15 +252,6 @@ dependencies = [ ] [[package]] -name = "crc32fast" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "738c290dfaea84fc1ca15ad9c168d083b05a714e1efddd8edaab678dc28d2836" -dependencies = [ - "cfg-if", -] - -[[package]] name = "criterion" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -461,18 +439,6 @@ dependencies = [ ] [[package]] -name = "flate2" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" -dependencies = [ - "cfg-if", - "crc32fast", - "libc", - "miniz_oxide", -] - -[[package]] name = "flexi_logger" version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -835,12 +801,11 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.4.4" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082" dependencies = [ "adler", - "autocfg", ] [[package]] @@ -1772,10 +1737,10 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" name = "wire-gateway" version = "0.1.0" dependencies = [ - "async-compression", "base32", "bitcoin", "hyper", + "miniz_oxide", "rand", "serde", "serde_json", diff --git a/script/test_gateway.sh b/script/test_gateway.sh index cda1d06..aaed418 100644 --- a/script/test_gateway.sh +++ b/script/test_gateway.sh @@ -2,11 +2,15 @@ set -eu +# Create temp file +TEMP_FILE=$(mktemp) + # Cleanup to run whenever we exit function cleanup() { for n in `jobs -p`; do kill $n 2> /dev/null || true done + rm -f $TEMP_FILE wait } @@ -30,7 +34,7 @@ for n in `seq 1 50`; do done echo "" -echo "---- Gateway API -----" +echo "---- Gateway API -----" echo -n "Making wire transfer to exchange:" for n in `seq 1 9`; do @@ -81,7 +85,7 @@ for bad_payto in http://bitcoin/$ADDRESS payto://btc/$ADDRESS payto://bitcoin/$A done echo "" -echo -n "Bad bitcoin address..." +echo -n "Bad bitcoin address:" taler-exchange-wire-gateway-client -b $BANK_ENDPOINT -C payto://bitcoin/42$ADDRESS -a BTC:0.00042 2>&1 | grep -q "(400/26)" && echo " OK" || echo " Failed" echo -n "Bad transaction amount:" @@ -104,7 +108,7 @@ function check_delta() { } for endpoint in incoming outgoing; do - echo -n "History $endpoint" + echo -n "History $endpoint:" check_delta ${endpoint}?delta=-9 9 "seq 1 9" && echo -n " OK" || echo -n " Failed" check_delta ${endpoint}?delta=9 9 "seq 1 9" && echo -n " OK" || echo -n " Failed" check_delta ${endpoint}?delta=-4 4 "seq 6 9" && echo -n " OK" || echo -n " Failed" @@ -114,5 +118,21 @@ for endpoint in incoming outgoing; do echo "" done +echo "----- Security -----" + +# Generate big random file +printf 'HelloWorld%s' {1..1000} >> $TEMP_FILE + +echo -n "Handle huge body:" +test `curl -w %{http_code} -X POST -s -o /dev/null -d @$TEMP_FILE ${BANK_ENDPOINT}transfer` -eq 400 && echo " OK" || echo " Failed" + +echo -n "Handle body length liar:" +test `curl -w %{http_code} -X POST -H"Content-Length:1024" -s -o /dev/null -d @$TEMP_FILE ${BANK_ENDPOINT}transfer` -eq 400 && echo " OK" || echo " Failed" + +# Generate compression bomb +printf 'HelloWorld%s' {1..1000} | pigz -z9 >> $TEMP_FILE + +echo -n "Handle compression bomb:" +test `curl -w %{http_code} -X POST -H"Content-Encoding:deflate" -s -o /dev/null --data-binary @$TEMP_FILE ${BANK_ENDPOINT}transfer` -eq 400 && echo " OK" || echo " Failed" echo "All tests passed" diff --git a/wire-gateway/Cargo.toml b/wire-gateway/Cargo.toml index bb71613..8839323 100644 --- a/wire-gateway/Cargo.toml +++ b/wire-gateway/Cargo.toml @@ -11,13 +11,7 @@ test = [] # Http library
hyper = { version = "0.14.15", features = ["http1", "server", "runtime"] }
# Async runtime
-tokio = { version = "1.14.0", features = [
- "net",
- "macros",
- "rt-multi-thread",
- "io-std",
- "io-util",
-] }
+tokio = { version = "1.14.0", features = ["net", "macros", "rt-multi-thread"] }
# Serialization framework
serde = { version = "1.0.130", features = ["derive"] }
# Serialization helper
@@ -30,8 +24,8 @@ serde_urlencoded = "0.7.0" base32 = "0.4.0"
# Error macros
thiserror = "1.0.30"
-# Async friendly compression
-async-compression = { version = "0.3.8", features = ["tokio", "zlib"] }
+# Deflate compression
+miniz_oxide = "0.5.1"
# Rng
rand = { version = "0.8.4", features = ["getrandom"] }
# Url format
@@ -39,8 +33,8 @@ url = { version = "2.2.2", features = ["serde"] } # Async postgres client
tokio-postgres = { version = "0.7.5" }
# Logging
-taler-log = {path = "../taler-log"}
+taler-log = { path = "../taler-log" }
# TODO Put this behind a feature
# Bitcoin data structure
-bitcoin = "0.27.1"
\ No newline at end of file +bitcoin = "0.27.1"
diff --git a/wire-gateway/src/json.rs b/wire-gateway/src/json.rs index 368280d..eb55a09 100644 --- a/wire-gateway/src/json.rs +++ b/wire-gateway/src/json.rs @@ -1,52 +1,68 @@ -use async_compression::tokio::{bufread::ZlibDecoder, write::ZlibEncoder};
-use hyper::{header, http::request::Parts, Body, Response, StatusCode};
-use tokio::io::{AsyncReadExt, AsyncWriteExt};
+use hyper::{body::HttpBody, header, http::request::Parts, Body, Response, StatusCode};
+use miniz_oxide::inflate::TINFLStatus;
+
+const MAX_ALLOWED_RESPONSE_SIZE: u64 = 4 * 1024; // 4MB
#[derive(Debug, thiserror::Error)]
-pub enum ParseError {
+pub enum ParseBodyError {
#[error(transparent)]
Body(#[from] hyper::Error),
#[error(transparent)]
Json(#[from] serde_json::Error),
- #[error(transparent)]
- Deflate(#[from] tokio::io::Error),
+ #[error("deflate decompression error")]
+ Deflate,
+ #[error("body is suspiciously big")]
+ SuspiciousBody,
+ #[error("decompression is suspiciously big")]
+ SuspiciousCompression,
}
-pub async fn parse_json<J: serde::de::DeserializeOwned>(
+/// Parse json body, perform security check and decompression
+pub async fn parse_body<J: serde::de::DeserializeOwned>(
parts: &Parts,
body: Body,
-) -> Result<J, ParseError> {
+) -> Result<J, ParseBodyError> {
+ // Check announced body size
+ if body.size_hint().upper().unwrap_or(u64::MAX) > MAX_ALLOWED_RESPONSE_SIZE {
+ return Err(ParseBodyError::SuspiciousBody);
+ }
+ // Read body
let bytes = hyper::body::to_bytes(body).await?;
- let mut buf = Vec::new();
- let decompressed = if parts
+
+ // Decompress if necessary
+ if parts
.headers
.get(header::CONTENT_ENCODING)
.map(|it| it == "deflate")
.unwrap_or(false)
{
- let mut decoder = ZlibDecoder::new(bytes.as_ref());
- decoder.read_to_end(&mut buf).await?;
- &buf
+ let decompressed = miniz_oxide::inflate::decompress_to_vec_zlib_with_limit(
+ &bytes,
+ MAX_ALLOWED_RESPONSE_SIZE as usize,
+ )
+ .map_err(|s| match s {
+ TINFLStatus::HasMoreOutput => ParseBodyError::SuspiciousCompression,
+ _ => ParseBodyError::Deflate,
+ })?;
+ // Parse json
+ Ok(serde_json::from_slice(&decompressed)?)
} else {
- bytes.as_ref()
- };
-
- Ok(serde_json::from_slice(&decompressed)?)
+ // Parse json
+ Ok(serde_json::from_slice(&bytes)?)
+ }
}
#[derive(Debug, thiserror::Error)]
-pub enum JsonRespError {
+pub enum EncodeBodyError {
#[error(transparent)]
Json(#[from] serde_json::Error),
- #[error(transparent)]
- Deflate(#[from] tokio::io::Error),
}
-pub async fn json_response<J: serde::Serialize>(
+pub async fn encode_body<J: serde::Serialize>(
parts: &Parts,
status: StatusCode,
json: &J,
-) -> Result<Response<Body>, JsonRespError> {
+) -> Result<Response<Body>, EncodeBodyError> {
let json = serde_json::to_vec(json)?;
if parts
.headers
@@ -55,10 +71,7 @@ pub async fn json_response<J: serde::Serialize>( .map(|str| str.contains("deflate"))
.unwrap_or(false)
{
- let mut encoder = ZlibEncoder::new(Vec::new());
- encoder.write_all(&json).await?;
- encoder.shutdown().await?;
- let compressed = encoder.into_inner();
+ let compressed = miniz_oxide::deflate::compress_to_vec_zlib(&json, 6);
Ok(Response::builder()
.status(status)
.header(header::CONTENT_TYPE, "application/json")
diff --git a/wire-gateway/src/main.rs b/wire-gateway/src/main.rs index 7125b3d..59eb6dd 100644 --- a/wire-gateway/src/main.rs +++ b/wire-gateway/src/main.rs @@ -4,7 +4,7 @@ use hyper::{ service::{make_service_fn, service_fn},
Body, Error, Method, Response, Server, StatusCode,
};
-use json::parse_json;
+use json::parse_body;
use std::{process::exit, str::FromStr, time::Instant};
use taler_log::log::{error, info, log, Level};
use tokio_postgres::{Client, NoTls};
@@ -18,7 +18,7 @@ use wire_gateway::{ error_codes::ErrorCode,
};
-use crate::json::json_response;
+use crate::json::encode_body;
mod error;
mod json;
@@ -149,7 +149,7 @@ async fn router( let response = match parts.uri.path() {
"/transfer" => {
assert_method(&parts, Method::POST)?;
- let request: TransferRequest = parse_json(&parts, body).await.catch_code(
+ let request: TransferRequest = parse_body(&parts, body).await.catch_code(
StatusCode::BAD_REQUEST,
ErrorCode::GENERIC_PARAMETER_MALFORMED,
)?;
@@ -169,7 +169,7 @@ async fn router( let row = state.client.query_one("INSERT INTO tx_out (_date, amount, wtid, debit_acc, credit_acc, exchange_url, status) VALUES (now(), $1, $2, $3, $4, $5, $6) RETURNING id", &[
&request.amount.to_string(), &request.wtid.as_ref(), &SELF_PAYTO, &request.credit_account.to_string(), &request.exchange_base_url.to_string(), &0i16
]).await.unwrap();
- json_response(
+ encode_body(
parts,
StatusCode::OK,
&TransferResponse {
@@ -213,7 +213,7 @@ async fn router( credit_account: Url::parse(row.get(5)).unwrap(),
})
.collect();
- json_response(
+ encode_body(
parts,
StatusCode::OK,
&IncomingHistory {
@@ -254,7 +254,7 @@ async fn router( exchange_base_url: Url::parse(row.get(6)).unwrap(),
})
.collect();
- json_response(
+ encode_body(
parts,
StatusCode::OK,
&OutgoingHistory {
@@ -269,12 +269,12 @@ async fn router( // We do not check input as this is a test admin endpoint
assert_method(&parts, Method::POST).unwrap();
let request: wire_gateway::api_wire::AddIncomingRequest =
- parse_json(&parts, body).await.unwrap();
+ parse_body(&parts, body).await.unwrap();
let timestamp = Timestamp::now();
let row = state.client.query_one("INSERT INTO tx_in (_date, amount, reserve_pub, debit_acc, credit_acc) VALUES (now(), $1, $2, $3, $4) RETURNING id", &[
&request.amount.to_string(), &request.reserve_pub.as_ref(), &request.debit_account.to_string(), &"payto://bitcoin/bcrt1qgkgxkjj27g3f7s87mcvjjsghay7gh34cx39prj"
]).await.unwrap();
- json_response(
+ encode_body(
parts,
StatusCode::OK,
&TransferResponse {
|