Compare commits

..

4 Commits

Author SHA1 Message Date
Xavier Roche
4020cda340 Remove the pre-3.31 (.dat/.ndx) cache import
The zip cache replaced this format in 3.31 (2003); what remained was an
import-only parser for hostile input, compiled unconditionally, plus a
tail of dead code keyed to it: the CACHE-1.5 writer behind if(1), the
cross-session HTML-MD5 dedup (its store was never written in the zip
era), the legacy legs of the startup rotation (which silently renamed
or deleted a user's .dat/.ndx pair) and of hts_cache_reconcile.

cache_init now detects a legacy pair, logs a clear refusal, leaves the
files untouched, and re-crawls. The cache-legacy self-test pins refusal
for both layouts (--update old.* and ro new.*), no is_update flag, and
untouched files; proven end-to-end against the local server. The
cacheindex self-test and fuzz-cachendx stay: cache_brstr/cache_binput
still back the -#C cache listing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-09 21:45:34 +02:00
Xavier Roche
a707d4b845 Add libFuzzer harnesses for the HTTP header and cache-index parsers (reland) (#509)
* Add libFuzzer harnesses for the HTTP header and cache-index parsers

P3-5 fuzz Tier-2. Two more harnesses over hostile-input parsers that read
structured bytes into fixed buffers: fuzz-header drives treatfirstline plus
treathead on each response-header line (the Content-Type/-Encoding path hardened
in #506, and the cookie/Location/Content-Range fields); fuzz-cachendx drives the
hts-cache/*.ndx length-prefixed scan that cache_readex_new loads on --update.

fuzz-cachendx found the over-advance fixed in the parent commit; its seed corpus
carries the two crash reproducers as replay regressions. The cache harness stops
at the scan rather than the trailing coucal insert, whose murmur hash trips a
separate pointer-overflow the .ndx parser does not own.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Correct copyright year on the new fuzz harnesses (2026)

fuzz-header.c and fuzz-cachendx.c are new in the 2026 audit cycle;
match the sibling *_selftest.c files rather than the boilerplate 1998.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 21:44:19 +02:00
Xavier Roche
65c1016a4e Bound the cache-index (.ndx) parser to its buffer (#507)
* Bound the cache-index (.ndx) parser to its buffer

cache_brstr trusted the on-disk length prefix for its cursor advance
(off += i, i up to 32768), so a corrupt or truncated hts-cache/*.ndx
whose declared length overstates the file walked the length-prefixed scan
past the end of the readfile() buffer; the next binput()/strchr() in the
loader then read out of bounds. A cache truncated by a crash or a full
disk, or one handed to --update/--continue, reaches it.

Bound both the copy and the advance in cache_brstr to the bytes actually
present (strnlen up to the terminating NUL), and add cache_binput(), a
binput() that refuses to start a read at or past end-of-buffer, for the
two loader scan loops in htscache.c and htscoremain.c; the latter also
gains the a < end guard the former already had. Regression: -#test=cacheindex
plus 01_engine-cacheindex.test (the deterministic bound check fails on the
old advance; the ASan CI jobs also exercise the scan).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* gitignore: ignore dist/ release staging artifacts

httrack-gh-release stages its signed tarballs in dist/ inside the
checkout; only root-level /httrack-*.tar.gz was ignored, so a broad
git add swept the 3.49.10/3.49.11 artifacts into this branch's first
commit (since rewritten out). Ignore the directory.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 21:02:27 +02:00
Xavier Roche
fd2252dc8c Fix remote stack overflow and uninit read in Content-Type/-Encoding parsing (#506)
treathead() parsed the Content-Type and Content-Encoding response-header
values with a width-less sscanf("%s") into a 1100-byte stack scratch,
bounds-checking the result only after the copy. The receive path in
back_wait hands treathead a line buffer of up to 2000 bytes (htsback.c
rcvd[2048], binput cap 2000), so a hostile server sending a header value
longer than 1099 non-whitespace bytes overflowed the stack. Bound both
scans to the buffer (%1099s), matching the existing %31s/%255s idiom in
the request-line parsers.

The Content-Encoding branch also ignored the sscanf return, so an empty
value (e.g. "Content-Encoding:") left tempo uninitialized and then ran
strlen() over it: an uninitialized read that can also over-read past the
buffer. The Content-Type branch already guarded this; mirror it.

Found via the P3-3 static-analysis baseline. New -#test=headerlong plus
empty-value cases in 01_engine-header.test build the hostile inputs in
the handler (the CLI caps argument length); they abort on the pre-fix
binary under ASan (overflow) and MSan (uninitialized read).

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 08:53:57 +02:00
21 changed files with 479 additions and 1022 deletions

3
.gitignore vendored
View File

@@ -34,8 +34,9 @@ Makefile
*.so.*
*.a
# make dist output.
# make dist output; dist/ holds httrack-gh-release staging artifacts.
/httrack-*.tar.gz
/dist/
# Editor / autotools backup files.
*~

View File

@@ -1,7 +1,7 @@
# libFuzzer harnesses; built only with --enable-fuzzers (requires clang).
if FUZZERS
noinst_PROGRAMS = fuzz-charset fuzz-meta fuzz-idna fuzz-entities \
fuzz-unescape fuzz-filters fuzz-url
fuzz-unescape fuzz-filters fuzz-url fuzz-header fuzz-cachendx
endif
AM_CPPFLAGS = \
@@ -23,6 +23,8 @@ fuzz_entities_SOURCES = fuzz-entities.c fuzz.h
fuzz_unescape_SOURCES = fuzz-unescape.c fuzz.h
fuzz_filters_SOURCES = fuzz-filters.c fuzz.h
fuzz_url_SOURCES = fuzz-url.c fuzz.h
fuzz_header_SOURCES = fuzz-header.c fuzz.h
fuzz_cachendx_SOURCES = fuzz-cachendx.c fuzz.h
# List corpus files explicitly: automake does not expand EXTRA_DIST globs.
EXTRA_DIST = README.md run-fuzzers.sh \
@@ -35,4 +37,8 @@ EXTRA_DIST = README.md run-fuzzers.sh \
corpus/filters/filter.bin corpus/filters/filter-size.bin \
corpus/filters/regress-empty-subject-unique.bin \
corpus/url/http-url.txt corpus/url/relative-path.txt \
corpus/url/regress-file-empty-path.txt corpus/url/regress-long-path-abort.txt
corpus/url/regress-file-empty-path.txt corpus/url/regress-long-path-abort.txt \
corpus/header/full-response.txt corpus/header/redirect.txt \
corpus/cachendx/new-format.txt corpus/cachendx/old-format.txt \
corpus/cachendx/regress-overadvance.bin \
corpus/cachendx/regress-truncated-entry.bin

View File

@@ -0,0 +1,7 @@
8
CACHE-1.1
28
Mon, 01 Jan 2024 00:00:00 GMT
www.example.com
/index.html
123

View File

@@ -0,0 +1,5 @@
3
1.0
www.example.com
/page
5

View File

@@ -0,0 +1,2 @@
32768
CACHE-1.1

View File

@@ -0,0 +1,5 @@
8
CACHE-1.1
1
x
www.example.com

View File

@@ -0,0 +1,10 @@
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 1234
Content-Encoding: gzip
Last-Modified: Mon, 01 Jan 2024 00:00:00 GMT
Etag: "abc"
Location: http://example.com/x
Set-Cookie: ID=42; path=/; domain=.example.com
Content-Range: bytes 0-99/100
Transfer-Encoding: chunked

View File

@@ -0,0 +1,3 @@
HTTP/1.0 301 Moved
Location: /elsewhere
Content-Disposition: attachment; filename="a.pdf"

65
fuzz/fuzz-cachendx.c Normal file
View File

@@ -0,0 +1,65 @@
/* ------------------------------------------------------------ */
/*
HTTrack Website Copier, Offline Browser for Windows and Unix
Copyright (C) 2026 Xavier Roche and other contributors
SPDX-License-Identifier: GPL-3.0-or-later
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Ethical use: we kindly ask that you NOT use this software to harvest email
addresses or to collect any other private information about people. Doing so
would dishonor our work and waste the many hours we have spent on it.
Please visit our Website: http://www.httrack.com
*/
/* Fuzz the cache-index (.ndx) parser: a corrupt or truncated index must not
walk the length-prefixed cache_brstr/cache_binput scan past the buffer.
Mirrors the -#C cache-listing scan (htscoremain.c). */
#include "fuzz.h"
#include "htscache.h"
#include "htslib.h"
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
char *buf = fuzz_strdup(data, size);
const char *const end = buf + size;
char firstline[256];
char *a = buf;
/* header: two length-prefixed fields (version, last-modified) */
a += cache_brstr(a, firstline, sizeof(firstline));
a += cache_brstr(a, firstline, sizeof(firstline));
/* body: newline-delimited host/file/position triples; the length-prefixed
scan must stay inside the buffer */
while (a != NULL && a < end) {
char BIGSTK line[HTS_URLMAXSIZE * 2];
char linepos[256];
int pos;
a = strchr(a + 1, '\n');
if (a == NULL)
break;
a++;
a += cache_binput(a, end, line, HTS_URLMAXSIZE);
a += cache_binput(a, end, line + strlen(line), HTS_URLMAXSIZE);
a += cache_binput(a, end, linepos, 200);
sscanf(linepos, "%d", &pos);
(void) pos;
}
freet(buf);
return 0;
}

70
fuzz/fuzz-header.c Normal file
View File

@@ -0,0 +1,70 @@
/* ------------------------------------------------------------ */
/*
HTTrack Website Copier, Offline Browser for Windows and Unix
Copyright (C) 2026 Xavier Roche and other contributors
SPDX-License-Identifier: GPL-3.0-or-later
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Ethical use: we kindly ask that you NOT use this software to harvest email
addresses or to collect any other private information about people. Doing so
would dishonor our work and waste the many hours we have spent on it.
Please visit our Website: http://www.httrack.com
*/
/* Fuzz the HTTP response-header parser (htslib.c): treatfirstline on the
status line, treathead on each following header. Both consume raw bytes
off the wire and copy fields into fixed htsblk buffers; treathead also
mutates its line in place and drives cookie parsing. */
#include "fuzz.h"
#include "htslib.h"
#include "htsbauth.h"
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
char *buf = fuzz_strdup(data, size);
htsblk r;
t_cookie *cookie = calloct(1, sizeof(*cookie));
char *line = malloct(size + 1);
char *p = buf;
int first = 1;
memset(&r, 0, sizeof(r));
cookie->max_len = (int) sizeof(cookie->data);
r.location = malloct(HTS_URLMAXSIZE * 2);
r.location[0] = '\0';
/* feed one header line at a time, as the receive loop does */
while (p != NULL && *p != '\0') {
char *nl = strchr(p, '\n');
size_t n = (nl != NULL) ? (size_t) (nl - p) : strlen(p);
memcpy(line, p, n);
line[n] = '\0';
if (first) {
treatfirstline(&r, line);
first = 0;
} else {
treathead(cookie, "www.example.com", "/", &r, line);
}
p = (nl != NULL) ? nl + 1 : NULL;
}
freet(r.location);
freet(line);
freet(cookie);
freet(buf);
return 0;
}

File diff suppressed because it is too large Load Diff

View File

@@ -89,14 +89,11 @@ typedef enum {
the involved files are absent. */
void hts_cache_reconcile(httrackp *opt, hts_cache_reconcile_mode mode);
int cache_writedata(FILE * cache_ndx, FILE * cache_dat, const char *str1,
const char *str2, char *outbuff, int len);
int cache_readdata(cache_back * cache, const char *str1, const char *str2,
char **inbuff, int *len);
void cache_rstr(FILE *fp, char *s, size_t s_size);
char *cache_rstr_addr(FILE * fp);
int cache_brstr(char *adr, char *s, size_t s_size);
/* binput over a NUL-terminated buffer, bounded: no read starts at/past end. */
int cache_binput(char *adr, const char *end, char *s, int max);
int cache_brint(char *adr, int *i);
void cache_rint(FILE * fp, int *i);
void cache_rLLint(FILE * fp, LLint * i);

View File

@@ -78,14 +78,6 @@ static void selftest_open_for_read(cache_back *cache, httrackp *opt) {
}
static void selftest_close(cache_back *cache) {
if (cache->dat != NULL) {
fclose(cache->dat);
cache->dat = NULL;
}
if (cache->ndx != NULL) {
fclose(cache->ndx);
cache->ndx = NULL;
}
if (cache->zipOutput != NULL) {
zipClose(cache->zipOutput,
"Created by HTTrack Website Copier (cache self-test)");
@@ -989,26 +981,15 @@ int cache_reconcile_selftest(httrackp *opt, const char *dir) {
failures +=
reconcile_expect(opt, "hts-cache/old.zip", SOLID, "promote-zip-noop");
/* PROMOTE: a pure-legacy old generation is promoted too (was dead when no
zip cache existed) */
/* PROMOTE: the removed pre-3.31 legacy pair is left alone */
reconcile_wipe(opt);
reconcile_put(opt, "hts-cache/old.dat", SOLID);
reconcile_put(opt, "hts-cache/old.ndx", TINY);
hts_cache_reconcile(opt, CACHE_RECONCILE_PROMOTE);
failures += reconcile_expect(opt, "hts-cache/new.dat", SOLID, "promote-dat");
failures += reconcile_expect(opt, "hts-cache/new.ndx", TINY, "promote-dat");
failures += reconcile_expect(opt, "hts-cache/old.dat", -1, "promote-dat");
/* PROMOTE: a half-written legacy new pair is replaced by the old pair */
reconcile_wipe(opt);
reconcile_put(opt, "hts-cache/new.dat", TINY);
reconcile_put(opt, "hts-cache/old.dat", SOLID);
reconcile_put(opt, "hts-cache/old.ndx", TINY);
hts_cache_reconcile(opt, CACHE_RECONCILE_PROMOTE);
failures +=
reconcile_expect(opt, "hts-cache/new.dat", SOLID, "promote-dat-partial");
failures +=
reconcile_expect(opt, "hts-cache/new.ndx", TINY, "promote-dat-partial");
failures += reconcile_expect(opt, "hts-cache/new.dat", -1, "promote-dat");
failures += reconcile_expect(opt, "hts-cache/new.ndx", -1, "promote-dat");
failures += reconcile_expect(opt, "hts-cache/old.dat", SOLID, "promote-dat");
failures += reconcile_expect(opt, "hts-cache/old.ndx", TINY, "promote-dat");
/* INTERRUPTED: no lock file, no action */
reconcile_wipe(opt);
@@ -1058,7 +1039,7 @@ int cache_reconcile_selftest(httrackp *opt, const char *dir) {
failures +=
reconcile_expect(opt, "hts-cache/new.zip", MID, "interrupted-bignew");
/* INTERRUPTED: the legacy pair follows the same size rule (was dead code) */
/* INTERRUPTED: the removed pre-3.31 legacy pair is left alone */
reconcile_wipe(opt);
reconcile_put(opt, "hts-in_progress.lock", 0);
reconcile_put(opt, "hts-cache/new.dat", TINY);
@@ -1067,9 +1048,13 @@ int cache_reconcile_selftest(httrackp *opt, const char *dir) {
reconcile_put(opt, "hts-cache/old.ndx", MID);
hts_cache_reconcile(opt, CACHE_RECONCILE_INTERRUPTED);
failures +=
reconcile_expect(opt, "hts-cache/new.dat", SOLID, "interrupted-dat");
reconcile_expect(opt, "hts-cache/new.dat", TINY, "interrupted-dat");
failures +=
reconcile_expect(opt, "hts-cache/new.ndx", MID, "interrupted-dat");
reconcile_expect(opt, "hts-cache/new.ndx", TINY, "interrupted-dat");
failures +=
reconcile_expect(opt, "hts-cache/old.dat", SOLID, "interrupted-dat");
failures +=
reconcile_expect(opt, "hts-cache/old.ndx", MID, "interrupted-dat");
/* ROLLBACK: the old zip generation is restored (a zip cache used to lose
its only good generation here) */
@@ -1089,17 +1074,18 @@ int cache_reconcile_selftest(httrackp *opt, const char *dir) {
failures += reconcile_expect(opt, "hts-cache/new.lst", MID, "rollback-lst");
failures += reconcile_expect(opt, "hts-cache/new.txt", MID, "rollback-txt");
/* ROLLBACK: full legacy generation incl. sidecars (historical behavior) */
/* ROLLBACK: sidecars restored, the removed pre-3.31 pair left alone */
reconcile_wipe(opt);
reconcile_put(opt, "hts-cache/new.dat", TINY);
reconcile_put(opt, "hts-cache/new.ndx", TINY);
reconcile_put(opt, "hts-cache/old.dat", SOLID);
reconcile_put(opt, "hts-cache/old.ndx", MID);
reconcile_put(opt, "hts-cache/old.lst", MID);
reconcile_put(opt, "hts-cache/old.txt", MID);
hts_cache_reconcile(opt, CACHE_RECONCILE_ROLLBACK);
failures += reconcile_expect(opt, "hts-cache/new.dat", SOLID, "rollback-dat");
failures += reconcile_expect(opt, "hts-cache/new.ndx", MID, "rollback-dat");
failures += reconcile_expect(opt, "hts-cache/new.dat", TINY, "rollback-dat");
failures += reconcile_expect(opt, "hts-cache/new.ndx", -1, "rollback-dat");
failures += reconcile_expect(opt, "hts-cache/old.dat", SOLID, "rollback-dat");
failures += reconcile_expect(opt, "hts-cache/old.ndx", MID, "rollback-dat");
failures += reconcile_expect(opt, "hts-cache/new.lst", MID, "rollback-dat");
failures += reconcile_expect(opt, "hts-cache/new.txt", MID, "rollback-dat");
@@ -1113,6 +1099,101 @@ int cache_reconcile_selftest(httrackp *opt, const char *dir) {
return failures;
}
/* The pre-3.31 .dat/.ndx import was removed: cache_init must refuse the
legacy pair, leave the files in place, and not flag an update run. */
int cache_legacy_refused_selftest(httrackp *opt, const char *dir) {
int failures = 0;
int variant;
selftest_tag = "cache-legacy";
golden_setup(opt, dir);
#ifdef _WIN32
mkdir(reconcile_st_path(opt, "hts-cache"));
#else
mkdir(reconcile_st_path(opt, "hts-cache"), HTS_PROTECT_FOLDER);
#endif
/* variant 0: old.dat/old.ndx (--update layout); 1: new.dat/new.ndx (ro) */
for (variant = 0; variant < 2; variant++) {
cache_back cache;
const char *const dat =
variant == 0 ? "hts-cache/old.dat" : "hts-cache/new.dat";
const char *const ndx =
variant == 0 ? "hts-cache/old.ndx" : "hts-cache/new.ndx";
reconcile_wipe(opt);
reconcile_put(opt, dat, 4096);
reconcile_put(opt, ndx, 4096);
opt->is_update = 0;
selftest_open(&cache, opt, /*ro*/ variant == 1);
if (cache_readable(&cache)) {
printf("cache-legacy: variant %d imported a removed format\n", variant);
failures++;
}
if (coucal_nitems(cache.hashtable) != 0) { /* no phantom index entries */
printf("cache-legacy: variant %d imported index entries\n", variant);
failures++;
}
if (opt->is_update) {
printf("cache-legacy: variant %d flagged an update\n", variant);
failures++;
}
if (fsize(reconcile_st_path(opt, dat)) != 4096 ||
fsize(reconcile_st_path(opt, ndx)) != 4096) {
printf("cache-legacy: variant %d touched the legacy files\n", variant);
failures++;
}
if (cache.lst != NULL) {
fclose(cache.lst);
cache.lst = NULL;
}
if (cache.txt != NULL) {
fclose(cache.txt);
cache.txt = NULL;
}
selftest_close(&cache);
}
/* a healthy zip must win over stray legacy files, with no refusal */
{
cache_back cache;
const char *const body = "<html>zip wins</html>";
reconcile_wipe(opt);
selftest_open_for_write(&cache, opt);
store_entry(opt, &cache, "example.com", "/", "example.com/index.html", 200,
"OK", "text/html", "utf-8", "", "", "", "", body, strlen(body));
selftest_close(&cache);
reconcile_put(opt, "hts-cache/old.ndx", 4096);
reconcile_put(opt, "hts-cache/old.dat", 4096);
selftest_open_for_read(&cache, opt);
if (!cache_readable(&cache)) {
printf("cache-legacy: stray legacy files shadowed the zip cache\n");
failures++;
}
failures +=
check_entry(opt, &cache, "example.com", "/", 200, "OK", "text/html",
"utf-8", "", "", "", "", body, strlen(body));
if (fsize(reconcile_st_path(opt, "hts-cache/old.ndx")) != 4096 ||
fsize(reconcile_st_path(opt, "hts-cache/old.dat")) != 4096) {
printf("cache-legacy: zip-wins pass touched the legacy files\n");
failures++;
}
if (cache.lst != NULL) {
fclose(cache.lst);
cache.lst = NULL;
}
if (cache.txt != NULL) {
fclose(cache.txt);
cache.txt = NULL;
}
selftest_close(&cache);
}
reconcile_wipe(opt);
return failures;
}
/* --- read-side corruption injection --------------------------------------- */
/* canary read back intact after each corruption; victim gets the byte surgery

View File

@@ -60,6 +60,10 @@ int cache_write_failure_selftest(httrackp *opt, const char *dir);
under <dir>. Returns the failed-check count. */
int cache_reconcile_selftest(httrackp *opt, const char *dir);
/* Verify cache_init refuses a pre-3.31 .dat/.ndx cache without touching it.
Returns the number of failed checks (0 = pass). */
int cache_legacy_refused_selftest(httrackp *opt, const char *dir);
/* Inject read-side corruption (zip byte surgery: bad size, header, deflate)
under <dir> and assert every case degrades to STATUSCODE_INVALID without
tainting a sibling entry. */

View File

@@ -138,14 +138,6 @@ RUN_CALLBACK0(opt, end); \
freet(cache.use); \
cache.use = NULL; \
} \
if (cache.dat) { \
fclose(cache.dat); \
cache.dat = NULL; \
} \
if (cache.ndx) { \
fclose(cache.ndx); \
cache.ndx = NULL; \
} \
if (cache.zipOutput) { \
zipClose(cache.zipOutput, \
"Created by HTTrack Website Copier/" HTTRACK_VERSION); \
@@ -155,10 +147,6 @@ RUN_CALLBACK0(opt, end); \
unzClose(cache.zipInput); \
cache.zipInput = NULL; \
} \
if (cache.olddat) { \
fclose(cache.olddat); \
cache.olddat = NULL; \
} \
if (cache.lst) { \
fclose(cache.lst); \
cache.lst = opt->state.strc.lst = NULL; \
@@ -488,32 +476,11 @@ void hts_finish_makeindex(httrackp *opt, int *makeindex_done,
*makeindex_done = 1;
}
/* Flush the parsed HTML output buffer to disk, skipping the rewrite when the
* on-disk MD5 is unchanged. */
/* Flush the parsed HTML output buffer to disk. */
void hts_finish_html_file(httrackp *opt, cache_back *cache, htsblk *r,
FILE **fp, const char *ht_buff, size_t ht_len,
const char *adr, const char *fil, const char *save) {
char digest[32 + 2];
off_t fsize_old =
fsize(fconv(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt), save));
int ok = 0;
digest[0] = '\0';
domd5mem(ht_buff, ht_len, digest, 1);
if (fsize_old == (off_t) ht_len) {
int mlen = 0;
char *mbuff;
cache_readdata(cache, "//[HTML-MD5]//", save, &mbuff, &mlen);
if (mlen)
mbuff[mlen] = '\0';
if ((mlen == 32) && (strcmp(((mbuff != NULL) ? mbuff : ""), digest) == 0)) {
ok = 1;
hts_log_print(opt, LOG_DEBUG, "File not re-written (md5): %s", save);
}
freet(mbuff);
}
if (!ok) {
{
file_notify(opt, adr, fil, save, 1, 1, r->notmodified);
*fp = filecreate(&opt->state.strc, save);
if (*fp) {
@@ -545,13 +512,7 @@ void hts_finish_html_file(httrackp *opt, cache_back *cache, htsblk *r,
if (fcheck)
hts_log_print(opt, LOG_ERROR, "* * Fatal write error, giving up");
}
} else {
file_notify(opt, adr, fil, save, 0, 0, r->notmodified);
filenote(&opt->state.strc, save, NULL);
}
if (cache->ndx)
cache_writedata(cache->ndx, cache->dat, "//[HTML-MD5]//", save, digest,
(int) strlen(digest));
}
/* does it look like XML ? (SVG et al.) */

View File

@@ -193,7 +193,7 @@ struct cache_back {
/* */
int type;
int ro; /**< read-only: no new cache is written */
FILE *dat, *ndx, *olddat; /**< new data, new index, old data files */
char *use; /**< in-memory list of cached adr+fil keys */
FILE *lst; /**< file list, used for purge */
FILE *txt; /**< human-readable file list (info) */
@@ -288,12 +288,12 @@ struct filecreate_params {
/* True if a new cache is being written (plain or zip backend). */
HTS_STATIC int cache_writable(cache_back * cache) {
return (cache != NULL && (cache->dat != NULL || cache->zipOutput != NULL));
return (cache != NULL && cache->zipOutput != NULL);
}
/* True if an old cache is available to read (plain or zip backend). */
HTS_STATIC int cache_readable(cache_back * cache) {
return (cache != NULL && (cache->olddat != NULL || cache->zipInput != NULL));
return (cache != NULL && cache->zipInput != NULL);
}
#endif

View File

@@ -1953,18 +1953,20 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
char BIGSTK url[HTS_URLMAXSIZE * 2];
char linepos[256];
int pos;
char *cacheNdx =
readfile(fconcat
(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt), StringBuff(opt->path_log),
"hts-cache/new.ndx"));
LLint cacheNdxLen = 0;
char *cacheNdx = readfile2(
fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
StringBuff(opt->path_log), "hts-cache/new.ndx"),
&cacheNdxLen);
cache_init(&cache, opt); /* load cache */
if (cacheNdx != NULL) {
char firstline[256];
char *a = cacheNdx;
const char *const end = cacheNdx + cacheNdxLen;
a += cache_brstr(a, firstline, sizeof(firstline));
a += cache_brstr(a, firstline, sizeof(firstline));
while(a != NULL) {
while (a != NULL && a < end) {
a = strchr(a + 1, '\n'); /* start of line */
if (a) {
htsblk r;
@@ -1972,15 +1974,15 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
/* */
a++;
/* read "host/file" */
a += binput(a, afs.af.adr, HTS_URLMAXSIZE);
a += binput(a, afs.af.fil, HTS_URLMAXSIZE);
a += cache_binput(a, end, afs.af.adr, HTS_URLMAXSIZE);
a += cache_binput(a, end, afs.af.fil, HTS_URLMAXSIZE);
url[0] = '\0';
if (!link_has_authority(afs.af.adr))
strcatbuff(url, "http://");
strcatbuff(url, afs.af.adr);
strcatbuff(url, afs.af.fil);
/* read position */
a += binput(a, linepos, 200);
a += cache_binput(a, end, linepos, 200);
sscanf(linepos, "%d", &pos);
if (!hasFilter
|| (strjoker(url, filter, NULL, NULL) != NULL)

View File

@@ -46,6 +46,7 @@ Please visit our Website: http://www.httrack.com
#include "htsdefines.h"
#include "htslib.h"
#include "htsparse.h"
#include "htscache.h"
#include "htscache_selftest.h"
#include "htsdns_selftest.h"
#include "htscharset.h"
@@ -1390,6 +1391,78 @@ static int st_cache(httrackp *opt, int argc, char **argv) {
return err;
}
/* A corrupt cache index (.ndx) must not walk the length-prefixed scan past
the buffer. Checks the two primitives the loader is built from. */
static int st_cacheindex(httrackp *opt, int argc, char **argv) {
int fail = 0;
(void) opt;
(void) argc;
(void) argv;
/* A length prefix that overstates the bytes present must bound the advance
to the buffer, not trust the declared length. */
{
static const char src[] = "32768\nCACHE-1.1";
const size_t len = sizeof(src) - 1;
char *buf = malloct(len + 1);
char s[256];
int off;
memcpy(buf, src, len + 1);
off = cache_brstr(buf, s, sizeof(s));
if (off > (int) len) {
printf("cacheindex: over-advance off=%d len=%d\n", off, (int) len);
fail = 1;
}
if (strcmp(s, "CACHE-1.1") != 0) {
printf("cacheindex: value=%s\n", s);
fail = 1;
}
freet(buf);
}
/* cache_binput reads a field while in bounds, but refuses one starting at
or past end-of-buffer. */
{
char buf[8] = "ab\ncd";
const char *const end = buf + 5;
char s[16];
if (cache_binput(buf, end, s, sizeof(s)) != 3 || strcmp(s, "ab") != 0)
fail = 1; /* normal read: "ab" then the '\n', 3 bytes consumed */
if (cache_binput(end, end, s, sizeof(s)) != 0 || s[0] != '\0')
fail = 1;
}
/* Drive the full loader scan over a truncated index: a declared length that
overshoots plus a half-written entry. ASan aborts here on the pre-fix
scan; the cursor must never leave the buffer. */
{
static const char src[] = "9\nCACHE-1.1\n99\nwww.example.com\n/a";
const size_t len = sizeof(src) - 1;
char *buf = malloct(len + 1);
const char *const end = buf + len;
char line[256];
char *a = buf;
memcpy(buf, src, len + 1);
a += cache_brstr(a, line, sizeof(line));
a += cache_brstr(a, line, sizeof(line));
while (a != NULL && a < end) {
a = strchr(a + 1, '\n');
if (a == NULL)
break;
a++;
a += cache_binput(a, end, line, sizeof(line));
}
freet(buf);
}
printf("cacheindex: %s\n", fail ? "FAIL" : "OK");
return fail;
}
static int st_cache_golden(httrackp *opt, int argc, char **argv) {
int regen, err;
@@ -1427,6 +1500,18 @@ static int st_cache_corrupt(httrackp *opt, int argc, char **argv) {
return err;
}
static int st_cache_legacy(httrackp *opt, int argc, char **argv) {
int err;
if (argc < 1) {
fprintf(stderr, "cache-legacy: needs a directory\n");
return 1;
}
err = cache_legacy_refused_selftest(opt, argv[0]);
printf("cache-legacy: %s\n", err ? "FAIL" : "OK");
return err;
}
static int st_reconcile(httrackp *opt, int argc, char **argv) {
int err;
@@ -2213,12 +2298,16 @@ static const struct selftest_entry {
{"sniff", "<content-type> <hex:..|text>", "MIME magic consistency",
st_sniff},
{"cache", "<dir>", "cache read/write round-trip self-test", st_cache},
{"cacheindex", "", "cache-index (.ndx) parse must stay in bounds",
st_cacheindex},
{"cache-golden", "<dir> [regen]", "frozen cache-format read self-test",
st_cache_golden},
{"cache-writefail", "<dir>", "cache write-failure handling self-test",
st_cache_writefail},
{"reconcile", "<dir>", "cache generation reconcile policy self-test",
st_reconcile},
{"cache-legacy", "<dir>", "pre-3.31 legacy cache refusal self-test",
st_cache_legacy},
{"cache-corrupt", "<dir>", "cache read-side corruption self-test",
st_cache_corrupt},
{"dns", "", "DNS resolver/cache self-test", st_dns},

10
tests/01_engine-cacheindex.test Executable file
View File

@@ -0,0 +1,10 @@
#!/bin/bash
#
set -euo pipefail
# Cache-index (.ndx) parse must stay inside the buffer on a corrupt length
# prefix (ASan aborts here on the pre-fix binary; the OK check gates the
# non-sanitized build too).
test "$(httrack -O /dev/null -#test=cacheindex)" == 'cacheindex: OK'

View File

@@ -0,0 +1,23 @@
#!/bin/bash
#
# The pre-3.31 .dat/.ndx cache import was removed: cache_init must refuse the
# legacy pair and leave the files untouched (httrack -#test=cache-legacy <dir>).
set -eu
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
# the refusal errors land on stdout (no log file); pin them and the verdict
out=$(httrack -#test=cache-legacy "$dir" 2>/dev/null)
cnt=$(printf '%s\n' "$out" | grep -c 'no longer supported' || true)
test "$cnt" = 2 || {
echo "expected 2 refusal messages, got $cnt" >&2
exit 1
}
out=$(printf '%s\n' "$out" | tail -n1)
test "$out" = "cache-legacy: OK" || {
echo "expected 'cache-legacy: OK', got: $out" >&2
exit 1
}

View File

@@ -38,6 +38,7 @@ TESTS = \
01_engine-filterdual.test \
01_engine-ftp-line.test \
01_engine-ftp-userpass.test \
01_engine-cacheindex.test \
01_engine-hashtable.test \
01_engine-header.test \
01_engine-idna.test \
@@ -67,6 +68,7 @@ TESTS = \
01_zlib-acceptencoding.test \
01_zlib-cache.test \
01_zlib-cache-corrupt.test \
01_zlib-cache-legacy.test \
01_zlib-cache-golden.test \
01_zlib-cache-writefail.test \
01_zlib-savename-cached.test \