vcswatch reports that
this package seems to have new commits in its VCS but has
not yet updated debian/changelog. You should consider updating
the Debian changelog and uploading this new version into the archive.
Here are the relevant commit logs:
commit fbd825d4dde392991520b54b9832b8394a84c11e
Author: Xavier Roche <roche@httrack.com>
Date: Tue Aug 4 22:09:55 2026 +0200
A crawled link can inject header lines into the requests HTTrack sends (#1008)
* Percent-escape network-derived URL pieces in outgoing request headers
A crawled href carrying keeps raw CR and LF in the engine's link
buffers: htsparse.c escapes control bytes before hts_unescapeEntities() decodes
the character references, so the escape runs too early to see them. The request
line survived that because it re-escapes 'fil' at emission, but four other
interpolations in http_sendhead() emitted the same data raw, letting a crawled
page add header lines of its choosing to the requests HTTrack then sends.
Escape at emission with escape_check_url(), the same escaper the request line
already uses, so a poisoned URL now goes out percent-encoded in every field
that carries it. The scratch buffer is sized like the url_adr/url_fil buffers
it mirrors, so escaping can only shrink what reaches the request buffer.
Signed-off-by: Xavier Roche <xroche@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <xroche@gmail.com>
* Cover the Referer host escape, and state the buffer bound accurately
The proxied leaf pages had no outbound links, so a poisoned host never became
anyone's referer and the Referer host escape went ungraded: reverting it alone
still passed. Give those pages a link so the host reaches a Referer, which also
adds a positive control on the plain proxied host.
The scratch buffer comment claimed escaping only shrinks its input, which is
backwards: percent-escaping expands up to 3x. What holds is that the result is
capped at the source buffer's own size, so the worst case emitted is unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <xroche@gmail.com>
* Grade the two escapes the probe could not reach, and the bare-LF split
The checker split each captured request on CRLF only, so an injection
that used a bare LF stayed glued inside one line. It reported "none
carrying an injected header" on a wire that carried one; real servers
and proxies do terminate a header line on a bare LF. Split on LF and
strip the CR, the way a tolerant parser reads it.
The proxy leg asserted nothing about the Referer, and no leg reached
the ftp-through-proxy request line at all, so those two escapes were
graded only by the catch-all. A crawl change that stopped following
either link would have un-covered them silently, which is how the
referer host went ungraded the first time round. Give the proxied root
an ftp:// link, and assert that both requests happened and carry the
escaped authority.
The crawl status was discarded, so an assertf abort would have been
graded on whatever bytes it managed to send first. Fail on the deadline
and on death by signal, and add the missing signal trap so the probe
server cannot outlive an interrupted run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
* Do not name a signal for an exit status that may be a panic
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
---------
Signed-off-by: Xavier Roche <xroche@gmail.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
commit c36a5428d718dfae3af3e0b4f92ec42b2ac710c6
Author: Xavier Roche <roche@httrack.com>
Date: Tue Aug 4 22:04:59 2026 +0200
Release notes understate the #841 cache-header overflow (#1009)
The 3.49.16 entries call it an unbounded sprintf and count it as one of "two
ProxyTrack buffer overflows", but #934 bounded that block in the engine's own
cache_add too, where the field caps sum past the 8192-byte stack buffer and the
values come off the wire. Say so, so a reader does not conclude only proxytrack
was exposed.
Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
commit 2945f0a5e499e08d60bb8f03bf4f5bd96e17732a
Author: Xavier Roche <roche@httrack.com>
Date: Tue Aug 4 21:42:37 2026 +0200
The WebHTTrack progress panel renders crawled URLs into HTML and JavaScript unescaped (#988)
* Escape crawled URLs in the WebHTTrack progress panel
refresh.html interpolated ${info.state}, ${info.name} and ${info.file} as
raw HTML and ${info.url_sav} into a single-quoted JS literal, so a link
path holding < > or ' ran as script in the panel origin. Point them at
htsserver.c's html: and js: modes, and sweep the other templates for the
same shape. Widen 185's scan to any unescaped handler or element-body
interpolation, and add 186 to probe the rendered panel during a crawl.
Closes #973
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
* Pin the panel's name column in the escaping test
186 asserted only on the leaf of the crawled path, so info.name could go
back to raw and still pass: 185 cannot see it either, because ${unquoted:}
sits in the directive regex it skips. The link now carries metacharacters
in a directory component too, short enough to survive the engine's 40-char
elision of that column, and the poll breaks on either form so the positive
control stays honest.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
* Make test 186 executable, like its siblings
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
* Re-arm the output mode that ${do:end-if} clears
${do:end-if} sets outputmode to 0 rather than restoring what it replaced
(htsserver.c), so the if-not-empty block partway down step4.html's command
textarea drops the ${do:output-mode:html} armed above it, and the 17
${unquoted:} settings below render raw. Posting maxtime or url2 as
A</textarea><img src=x onerror=alert(1)>B breaks out of the textarea.
finished.html's <pre> has the same shape; those two are the only ones.
--sitemap-url moves from html: to arg: like its quoted siblings, since
cat_html_escaped() escapes neither " nor \.
Test 185 had unquoted: on its directive skip list, which is how all 17
sailed past it, and now also rejects an html: value on the command line.
Test 83 reads back the escaped forms at runtime.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
---------
Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
commit 2312d711e616337837f6ccd1ea1515b32d13872d
Author: Xavier Roche <roche@httrack.com>
Date: Tue Aug 4 21:37:14 2026 +0200
A control byte in a link makes the crawler fetch a URL the page never wrote (#1005)
* A vertical tab or form feed in a link is deleted, and the crawler fetches a URL the page never wrote
The link scan treated any byte below 32 as the end of a URL unless is_space()
accepted it, and is_space() counts VT and FF. Neither is among the CR, LF and
TAB stripped from the URL further down, so both survived into
escape_remove_control(), which deletes them: href="/a<VT>bc" resolved to /abc.
The other 28 control bytes ended the scan where they stood, so the crawler
fetched the truncated prefix instead.
A control byte the existing end-of-URL tests did not already consume now costs
the link. CR, LF and TAB keep their exemption because the parser strips them a
few lines later, and the check sits below that end-of-URL logic rather than
above it, so a VT or FF ending an unquoted attribute value still ends it.
Closes #982
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
* Percent-encode a link's control bytes instead of truncating or deleting them
A control byte inside a link ended the URL where it stood, so
href="/s<SOH>oh.html" fetched /s. VT and FF came out worse: is_space()
counts them, so the scan carried them into the URL, and neither is among
the CR, LF and TAB stripped further down, so escape_remove_control()
deleted them and href="/a<VT>bc" resolved to /abc.
Both now percent-encode as a browser does. The encoding runs on the
extracted link, where the destination capacity is visible, because a byte
grows to three; a link whose encoded form no longer fits is dropped rather
than clipped to a URL nobody wrote.
Closes #982
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
* Tighten the wording of the encode-site comment
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
* Assert that a link's CR, LF and TAB are still stripped
Test 158 covered the encoding half of the fix but not the stripping half,
on a line this branch rewrote. Making the strip unconditional, so CR, LF
and TAB percent-encode like every other control byte, left the whole suite
green. The new href carries all three and only its stripped target exists
on the server, so that mutant now fails on the 404 it provokes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
* Strip a link's end control bytes, and bound the query's growth too
Review of the encoding change found two holes.
A control byte at either end of a link used to end the scan there, which
threw away nothing and gave the right URL; encoding it instead turned
href="page.html<SOH>" into a 404. Both ends now strip every C0 control or
space, as the WHATWG URL parser does, so only interior bytes are encoded.
The leading loop stops on the terminator the trailing loop just wrote,
since '\0' is <= ' ' and it would otherwise read on into the previous
link's leftovers and fetch those.
The path bound missed the query, which is escaped separately and appended
back by a helper that clips silently. An absolute link whose path and
query together outgrow the buffer was landing just under the URL-length
gate and being fetched truncated; it is now dropped like the path case.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
---------
Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
commit 7bff8133bbe64546131542f87ee736aff006c62b
Author: Xavier Roche <roche@httrack.com>
Date: Tue Aug 4 21:10:38 2026 +0200
Two installed headers declare symbols the library does not export (#990)
* Two installed headers declare symbols the library does not export
htsbasenet.h declares openssl_ctx and htsnet.h declares
hts_dns_set_resolver_backend, both hidden by -fvisibility=hidden and both
installed into $(includedir)/httrack. A consumer that includes either header
and uses the name compiles, then fails to link.
Move both behind HTS_INTERNAL_BYTECODE, the guard the other internal
declarations in the installed set already use. Every in-tree caller defines
it, so the engine build is unaffected.
Test 207 installs the headers, derives the hidden set from the library's
symbol table minus its dynamic table, and links a probe for every hidden name
a consumer can reach through an installed header.
Closes #977
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
* Test 207's floor against a vacuous run counts only header-static symbols
Every one of the 31 symbols that reaches the link probe is a static function
defined in the installed header itself (abortf_ and the rest of htssafe.h,
StringOom_ from htsstrings.h). The probe object compiles its own copy, so the
link cannot fail whatever the library exports, and "probed >= 5" holds without
the harvest ever seeing an extern declaration. Cutting the header loop down to
htssafe.h alone leaves probed at 7, and the test still passes with 13 of the 14
headers unchecked.
So plant a leak: a canary header declaring the hidden symbol the negative
control has already shown cannot link. The loop has to report it, which puts
the install, the harvest, the intersection and the link on the same path a real
leak takes. The cut-down loop now fails.
The HTTRACK_SHLIB comment also blamed the skip on static-only builds. macOS
skips as well, where libtool names the library .dylib.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
* Trim test 207's comments to the house one-line default
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
* Declare struct addrinfo at file scope in htsnet.h
Without it the tag in the resolver-backend prototypes is a fresh type scoped
to its own declaration wherever <netdb.h> has not already declared it.
Closes #987
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
---------
Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
commit e5985903e3e3297fb4157ec91cf71fa62833a19a
Author: Xavier Roche <roche@httrack.com>
Date: Tue Aug 4 20:52:04 2026 +0200
The crash report can hang: the symbolizer forks from the signal handler (#985)
* Crash report can hang: the symbolizer forks from the signal handler
fork() runs the pthread_atfork prepare handlers before it forks, and glibc's
malloc registers one that takes every arena lock. A signal raised inside
malloc (glibc's own heap-corruption detector aborts from exactly there, and
SIGABRT is wired to sig_fatal) then leaves the handler blocked on a lock its
own thread holds, and the report is lost.
vfork() bypasses the atfork handlers, at the price of a child that may only
issue syscalls: the PATH search execvp() performs allocates, so the symbolizer
is resolved once at startup and the child execv()s an absolute path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
* Test 182 pins a 64-bit address width, so it fails the i386 leg
addr2line -a zero-pads the address to the target's pointer size: 8 nibbles
against a 32-bit ELF, 16 against a 64-bit one. The gate is "is addr2line
installed", not the architecture, so the {16} match fails a correct build on
the gcc -m32 leg and on Debian's 32-bit buildds. Test 80 already matches the
same line unanchored.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
* Spawn the symbolizer with posix_spawnp() instead of fork()
posix_spawnp() reaches the same child through clone(CLONE_VM|CLONE_VFORK),
which runs no pthread_atfork handler, so the arena lock a signal raised inside
malloc already holds cannot deadlock the report away.
It also does the PATH search itself and reports an exec failure through its
return value, so the hand-rolled find_on_path(), the two statics caching its
answer and the BT_NO_SYMBOLIZER exit protocol all go. Building the file actions
in hts_backtrace_init() keeps the only allocation off the crash path, and with
the rewrite happening in the parent the addr2line to llvm-symbolizer fallback
comes back.
Signed-off-by: Xavier Roche <xroche@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
* Assert the symbolizer output lands on the report fd
The child's stdout redirect is now a file actions object built at startup, so a
wrong target sends symbolized frames to the crawler's own stdout instead. The
existing checks merge both streams and cannot see that.
Signed-off-by: Xavier Roche <xroche@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
* Take the report fd out of hts_print_backtrace()'s signature
The child's stdout redirect is prebuilt at init against stderr, so passing any
other fd quietly skipped symbolization on a runtime check. The one caller
passed stderr, and htsbacktrace.h ships with the program and not with the
installed dev headers, so there is no ABI cost to making the wrong value
impossible to write.
Test 182 also picks up test 80's ARM gate. A build with no unwind tables
prints "unwinding failed" instead of the OS notice, which 182's skip check
misses, so it would have gone red on armhf rather than skipping.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
* Assert a symbol name in test 182, not just the frame shape
An all-"??" symbolization regression still emits the address lines.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
---------
Signed-off-by: Xavier Roche <roche@httrack.com>
Signed-off-by: Xavier Roche <xroche@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
commit ed8a376278a03fa9f30e0b1ececd4c4102d4f8e9
Author: Xavier Roche <roche@httrack.com>
Date: Tue Aug 4 20:19:30 2026 +0200
escape_remove_control() leaves the original tail glued to the result (#981)
* escape_remove_control() leaves the original tail glued to the result
It compacted the non-control bytes downward in place but never wrote the
new terminator, so any input it actually shortened came back as the
compacted head plus whatever the old tail left behind: "/a\013bc" came
out "/abcc". The function is HTSEXT_API and declared in the installed
httrack-library.h, so the broken contract is a public one.
The parser can reach it: VT and FF pass the link scan as is_space()
members and the later strip only removes CR, LF and TAB, so a page with
href="/a<VT>bc" fetched /abcc before this.
Closes #974
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
* escape-control self-test lets a shifted cut and a stray write through
Grading mutants against the new test found two survivors. No vector held
a space, so moving the loop's cut to `c > 32` stripped it and the suite
stayed green. Nothing reached above 0x0c either, so `c >= 13` passed too.
The added vectors pin both sides of the cut, and the second drops two
bytes instead of one.
The canary read one byte at `inlen + 1`, so a stray write two or more
past the compacted end went unseen. The buffer is already poisoned in
full, so scanning the rest of it costs nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
---------
Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>