Compare commits

..

3 Commits

Author SHA1 Message Date
Xavier Roche
95c7d3c044 Route the too-large chunk through the invalid-chunk teardown
Review found the first cut hand-rolled the abort (statuscode/status/finished)
and then fell through into the shared chunk dispatch, which re-ran finalize and
stomped the message; it was only safe by accident. Set chunk_size = -1 instead
and let the existing chunk_size < 0 path tear the transfer down (deletehttp +
deleteaddr + STATUSCODE_INVALID), matching the sibling error handling. Verified
by hand with a lowered cap: the resource is rejected as "Invalid chunk", the
socket is closed, and no partial file is written.

Also fix the test's decoded-vs-raw discriminator: '^40$' never matched the
CRLF-framed size line, and '</body></html>' is split by HTTrack's mirror
comment. Assert no CR (framing decoded) and '</body>' (no truncation).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-15 21:43:06 +02:00
Xavier Roche
755481e33b tests: mirror a chunked response (regression for the 2GB cap)
Add a Transfer-Encoding: chunked route to the test server and a crawl test that
mirrors it, asserting the chunk automaton joins the bodies (no leftover chunk
framing) and the new 2GB in-RAM cap stays quiet on ordinary traffic. The cap's
overflow threshold isn't reachable at test scale (it needs gigabytes on the
wire), so this pins the non-regression side; the abort path was verified by
hand with a lowered cap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-15 21:31:02 +02:00
Xavier Roche
83a3af7fc5 Cap the chunked-transfer buffer at 2GB before realloc
In the chunked-transfer automaton, back[i].r.totalsize is a 64-bit signed value
that accumulates the attacker-declared size of each chunk, and the in-memory
branch reallocs to (size_t) totalsize + 1 before the chunk body arrives. With no
upper bound the sum can exceed 2GB across chunks; cast to a 32-bit size_t that
truncates and under-allocates the buffer, so the following chunk data overflows
the heap. The sibling in-memory Content-Length path already caps at INT32_MAX;
apply the same bound here and abort the transfer (refetchable) when it trips.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-15 21:15:34 +02:00
4 changed files with 3 additions and 106 deletions

View File

@@ -46,12 +46,6 @@ jobs:
# fopen's umask-controlled 0666 is intended for mirror/cache/log
# output; the one credential file (cookies.txt) is kept 0600 on Unix.
config: |
paths-ignore:
# Demo callback samples, not part of libhttrack.
- libtest
# ProxyTrack: a separate legacy binary with no auth surface; its
# recv/cache-parse code trips taint queries by design.
- src/proxy
query-filters:
- exclude:
id: cpp/world-writable-file-creation

View File

@@ -2696,12 +2696,9 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
int dispo = 0;
// probe the resolved address list once per fresh connect (cache hit:
// the host was resolved when this connect was opened). Not under a
// proxy: the socket dials the proxy, so resolving the origin here leaks
// its DNS and lets a proxy-connect failure fall back to dialing it
// direct.
// the host was resolved when this connect was opened)
if (cf->addr_count < 0 && back[i].r.soc != INVALID_SOCKET &&
!back[i].r.is_file && !back[i].r.req.proxy.active) {
!back[i].r.is_file) {
SOCaddr scratch[HTS_MAXADDRNUM];
cf->addr_count = hts_dns_resolve_all(opt, back[i].url_adr, scratch,

View File

@@ -1,93 +0,0 @@
#!/bin/bash
#
# A crawl through a proxy must never resolve or dial the origin itself: the proxy
# does that. A dead proxy + a multi-address origin used to fall back to dialing
# the origin direct (bypassing the proxy, leaking its DNS and IP). The decoy
# origin here must therefore receive nothing.
set -euo pipefail
: "${top_srcdir:=..}"
# shellcheck source=tests/testlib.sh
. "$top_srcdir/tests/testlib.sh"
if test "${V6_SUPPORT:-}" == "no"; then
echo "no IPv6 support (resolver override compiled out), skipping"
exit 77
fi
python=$(find_python) || {
echo "python3 missing, skipping"
exit 77
}
server=$(nativepath "$top_srcdir/tests/local-server.py")
root=$(nativepath "$top_srcdir/tests/server-root")
tmpdir=$(mktemp -d)
serverpid=
cleanup() {
stop_server "$serverpid"
rm -rf "$tmpdir"
return 0
}
trap cleanup EXIT
# decoy origin: it must stay silent. LOCAL_SERVER_VERBOSE logs any request it gets.
LOCAL_SERVER_VERBOSE=1 "$python" "$server" --root "$root" --bind 127.0.0.1 \
>"$tmpdir/srv.out" 2>"$tmpdir/srv.err" &
serverpid=$!
port=
for _ in $(seq 1 50); do
line=$(head -n1 "$tmpdir/srv.out" 2>/dev/null || true)
if test "${line%% *}" == "PORT"; then
port="${line#PORT }"
break
fi
kill -0 "$serverpid" 2>/dev/null || {
echo "server exited early: $(cat "$tmpdir/srv.err")"
exit 1
}
sleep 0.1
done
test -n "$port" || {
echo "could not discover server port"
exit 1
}
# a proxy port nothing listens on: grab a free one and let it close (refuses).
deadproxy=$("$python" -c \
'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close()')
# origin resolves to two addresses so a fallback would have a second to dial;
# 127.0.0.1 (the decoy) is the one the old bypass reached.
out="$tmpdir/crawl"
HTTRACK_DEBUG_RESOLVE="decoyhost:127.0.0.2,127.0.0.1" \
httrack "http://decoyhost:$port/simple/basic.html" -O "$out" \
-P "127.0.0.1:$deadproxy" -c1 --robots=0 --timeout=5 --retries=0 --quiet -Z \
>"$tmpdir/log" 2>&1
log="$out/hts-log.txt"
# the origin must have seen no connection (no leak/bypass)
if grep -qE '"(GET|POST|HEAD|CONNECT) ' "$tmpdir/srv.err"; then
echo "FAIL: origin was contacted directly, bypassing the proxy"
cat "$tmpdir/srv.err"
exit 1
fi
# the crawl must fail at the proxy (proves it really tried the proxy, not a no-op)
grep -q 'Connect Error' "$log" || {
echo "FAIL: expected a proxy connect error"
cat "$log"
exit 1
}
# and it must not have tried an origin-address fallback under the proxy
if grep -q "trying next address" "$log"; then
echo "FAIL: fell back to an origin address under a proxy"
cat "$log"
exit 1
fi
echo "OK: a dead proxy fails cleanly without dialing the origin"

View File

@@ -132,7 +132,6 @@ TESTS = \
52_local-socks5.test \
53_local-proxytrack-cache-corrupt.test \
54_local-update-truncate-purge.test \
55_local-chunked.test \
56_local-proxy-noleak.test
55_local-chunked.test
CLEANFILES = check-network_sh.cache