Compare commits

...

3 Commits

Author SHA1 Message Date
Xavier Roche
a989aa5494 Tighten the #638 comments
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-22 09:10:34 +02:00
Xavier Roche
9382f1e7ac Exclude the auth-bypass CodeQL query and pin case-insensitive scheme matching
cpp/user-controlled-bypass models auth-bypass-by-spoofing, but httrack has no
auth or access-control surface; its nearest downstream decision, the +/- crawl
filter, is a mirror boundary, not a security one. Exclude it like the
world-writable query.

The stripport self-test's scheme detection is case-insensitive (strfield/streql)
but nothing pinned it: a case-sensitive rewrite would strip :80 from HTTPS://
unnoticed. Replace the non-discriminating :8443 case (no scheme defaults to 8443,
so it passes for any impl) with HTTPS://h:80, which must keep its port. Note at
scheme_default_port that schemeless/protocol-relative links default to 80.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-22 09:01:56 +02:00
Xavier Roche
132c9c1df6 Strip only the scheme's own default port, not :80 on every scheme
hts_strip_default_port treated 80 as the default for every scheme, so an
explicit :80 on a non-http URL was dropped and the fetch silently moved to that
scheme's real default: https://h:80/x became https://h/x (fetched on 443), ftp
likewise. The reverse gap: a scheme's own default (443 https, 21 ftp) was never
stripped, so https://h:443/x kept a redundant port and would not dedup against
the portless form.

Derive the default from lien's scheme (80 http, 443 https, 21 ftp; 80 when
absent or unknown) and strip only when the port equals that. Same family as
#627/#614.

Closes #638

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-22 08:27:40 +02:00
4 changed files with 29 additions and 10 deletions

View File

@@ -55,6 +55,9 @@ jobs:
query-filters:
- exclude:
id: cpp/world-writable-file-creation
# Models auth-bypass-by-spoofing; httrack has no auth surface, its +/- crawl filter is a mirror boundary, not a security one.
- exclude:
id: cpp/user-controlled-bypass
# Manual build: CodeQL traces the compiler, so build exactly what ships.
- name: Build

View File

@@ -315,9 +315,18 @@ static void escape_url_parens(char *const s, const size_t size) {
strlcpybuff(s, buff, size);
}
/* Strip a default ":80" from lien's authority in place. Any spelling that
range-parses to 80 (":80", ":080") is dropped by its matched length, not a
hardcoded 3 chars; a value that merely wraps to 80 as an int (#614) stays. */
/* Default port for lien's scheme (case-insensitive), 80 if absent/unknown; so
schemeless and protocol-relative //host links default to 80 (known gap). */
static int scheme_default_port(const char *lien) {
if (strfield(lien, "https:"))
return 443;
if (strfield(lien, "ftp:"))
return 21;
return 80;
}
/* Strip the scheme's own default port (80 http, 443 https, 21 ftp) from lien's
authority in place; :80 on https/ftp stays as a real port (#638, #614). */
void hts_strip_default_port(char *lien, size_t size) {
char *a;
@@ -339,7 +348,8 @@ void hts_strip_default_port(char *lien, size_t size) {
b++;
saved = *b;
*b = '\0';
is_default = hts_parse_url_port(a + 1, &port) && port == 80;
is_default =
hts_parse_url_port(a + 1, &port) && port == scheme_default_port(lien);
*b = saved;
if (is_default) { // default port, strip it
char BIGSTK tempo[HTS_URLMAXSIZE * 2];

View File

@@ -1589,10 +1589,8 @@ static int st_identabs(httrackp *opt, int argc, char **argv) {
return 0;
}
/* Default-port strip (#627): a genuine 80 (any spelling) is removed by its
matched length, host preserved; a non-80 port or one that only wraps to 80 as
a 32-bit int (#614) is left intact. Guards the old bug where ":080"/":0080"
dropped a hardcoded 3 chars and glued the leftover digits onto the host. */
/* Default-port strip is scheme-aware (#638), overflow-safe (#614): a scheme's
own default (any spelling) is dropped, a real port stays; guards #627. */
static int st_stripport(httrackp *opt, int argc, char **argv) {
static const struct {
const char *in, *out;
@@ -1606,6 +1604,13 @@ static int st_stripport(httrackp *opt, int argc, char **argv) {
{"http://127.0.0.1:8080/x", "http://127.0.0.1:8080/x"},
{"http://127.0.0.1:4294967376/x", "http://127.0.0.1:4294967376/x"},
{"http://127.0.0.1/x", "http://127.0.0.1/x"},
{"https://127.0.0.1:443/x", "https://127.0.0.1/x"},
{"https://127.0.0.1:80/x", "https://127.0.0.1:80/x"},
// Scheme match is case-insensitive: HTTPS' default is 443, so :80 stays.
{"HTTPS://127.0.0.1:80/x", "HTTPS://127.0.0.1:80/x"},
{"ftp://127.0.0.1:21/x", "ftp://127.0.0.1/x"},
{"ftp://127.0.0.1:80/x", "ftp://127.0.0.1:80/x"},
{"http://127.0.0.1:443/x", "http://127.0.0.1:443/x"},
};
size_t k;

View File

@@ -6,6 +6,7 @@ set -euo pipefail
# Stripping a default :80 from a crawled link used to skip a hardcoded 3 chars
# (#627): ":080"/":0080" lost only ":80" and glued the rest onto the host
# (127.0.0.1:080/x -> 127.0.0.10/x), and a value wrapping to 80 as a 32-bit int
# (#614) was stripped as if it were the default. All assertions live in the
# engine self-test.
# (#614) was stripped as if it were the default. The default is scheme-aware
# (#638): an explicit :80 on https/ftp stays, :443/:21 strip under their own
# scheme. All assertions live in the engine self-test.
httrack -O /dev/null -#test=stripport | grep -q "stripport self-test OK"