urlapi: same origin tests

Add new internal `curl_url_same_origin()` to check if a href has the
same origin as a base URL. Add test cases in test1675 and use this in
http2 push handling.

Closes #21328
This commit is contained in:
Stefan Eissing 2026-04-15 10:43:12 +02:00 committed by Daniel Stenberg
parent ce7174555d
commit 32a513e180
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
4 changed files with 126 additions and 13 deletions

View file

@ -718,8 +718,9 @@ static struct Curl_easy *h2_duphandle(struct Curl_cfilter *cf,
return second;
}
static int set_transfer_url(struct Curl_easy *data, bool via_ssl_conn,
struct curl_pushheaders *hp)
static int set_transfer_url(struct Curl_easy *newhandle,
struct curl_pushheaders *hp,
struct Curl_easy *data)
{
const char *v;
CURLUcode uc;
@ -732,14 +733,6 @@ static int set_transfer_url(struct Curl_easy *data, bool via_ssl_conn,
v = curl_pushheader_byname(hp, HTTP_PSEUDO_SCHEME);
if(v) {
if(!via_ssl_conn) {
/* PUSH over an insecure connection, accept only insecure schemes. */
const struct Curl_scheme *scheme = Curl_get_scheme(v);
if(!scheme || (scheme->flags & PROTOPT_SSL)) {
rc = 1;
goto fail;
}
}
uc = curl_url_set(u, CURLUPART_SCHEME, v, 0);
if(uc) {
rc = 1;
@ -765,6 +758,13 @@ static int set_transfer_url(struct Curl_easy *data, bool via_ssl_conn,
}
}
/* We can only allow PUSH of resource from the same origin, e.g.
* scheme + hostname + port */
if(!Curl_url_same_origin(data->state.uh, u)) {
rc = 1;
goto fail;
}
uc = curl_url_get(u, CURLUPART_URL, &url, 0);
if(uc)
rc = 4;
@ -773,7 +773,7 @@ fail:
if(rc)
return rc;
Curl_bufref_set(&data->state.url, url, 0, curl_free);
Curl_bufref_set(&newhandle->state.url, url, 0, curl_free);
return 0;
}
@ -819,8 +819,7 @@ static int push_promise(struct Curl_cfilter *cf,
heads.stream = stream;
heads.frame = frame;
rv = set_transfer_url(newhandle,
Curl_conn_is_ssl(cf->conn, cf->sockindex), &heads);
rv = set_transfer_url(newhandle, &heads, data);
if(rv) {
CURL_TRC_CF(data, cf, "[%d] PUSH_PROMISE, failed to set URL -> %d",
frame->promised_stream_id, rv);

View file

@ -63,4 +63,6 @@ CURLUcode Curl_junkscan(const char *url, size_t *urllen, bool allowspace);
#define U_CURLU_URLDECODE (unsigned int)CURLU_URLDECODE
#define U_CURLU_PATH_AS_IS (unsigned int)CURLU_PATH_AS_IS
bool Curl_url_same_origin(CURLU *base, CURLU *href);
#endif /* HEADER_CURL_URLAPI_INT_H */

View file

@ -1991,3 +1991,36 @@ nomem:
}
return CURLUE_OK;
}
bool Curl_url_same_origin(CURLU *base, CURLU *href)
{
const struct Curl_scheme *s = NULL;
/* base must be an absolute URL */
if(!base->scheme || !base->host)
return FALSE;
if(href->scheme && !curl_strequal(base->scheme, href->scheme))
return FALSE;
if(href->host) {
if(!curl_strequal(base->host, href->host))
return FALSE;
if(!curl_strequal(base->port, href->port)) {
/* This may still match if only one has an explicit port
* and it is the default for the scheme. */
if(base->port && href->port)
return FALSE;
s = Curl_get_scheme(base->scheme);
if(!s) /* Cannot match default port for unknown scheme */
return FALSE;
/* The port which is set must be the default one */
if((base->port && (base->portnum != s->defport)) ||
(href->port && (href->portnum != s->defport)))
return FALSE;
}
}
else if(href->port) /* no host in href, then there must be no port */
return FALSE;
return TRUE;
}

View file

@ -301,5 +301,84 @@ static CURLcode test_unit1675(const char *arg)
abort_if(fails, "parse_file tests failed");
}
/* Test same origin check. For now, we can only do that when
* schemes are supported by libcurl. */
#ifndef CURL_DISABLE_HTTP
{
CURLU *base, *href;
int fails = 0;
unsigned int i;
bool match;
struct origin_test {
const char *base;
const char *scheme;
const char *host;
const char *port;
const char *path;
bool expect_match;
};
const struct origin_test tests[] = {
{"http://host:123/x", "http", "host", "123", "/y", TRUE},
{"http://host:123/x", NULL, "host", "123", "/y", TRUE},
{"http://host:123/x", NULL, NULL, NULL, "/y", TRUE},
{"http://host:80/x", "http", "host", "123", "/y", FALSE},
{"http://host:80/x", "http", "host", NULL, "/y", TRUE},
{"http://host/x", "http", "host", "80", "/y", TRUE},
#ifdef USE_SSL
{"http://host:123/x", "https", "host", "123", "/y", FALSE},
{"https://host/x", "http", "host", "443", "/y", FALSE},
{"https://host/x", "https", "host", "443", "/y", TRUE},
#endif
};
for(i = 0; i < CURL_ARRAYSIZE(tests); i++) {
CURLUcode uc;
base = curl_url();
href = curl_url();
if(!base || !href) {
curl_mfprintf(stderr, "%d: failed to allocate memory\n", i);
fails++;
goto loop_end;
}
uc = curl_url_set(base, CURLUPART_URL, tests[i].base, 0);
if(uc) {
curl_mfprintf(stderr, "failed to parse %d base %s -> %d\n", i,
tests[i].base, uc);
fails++;
goto loop_end;
}
if(tests[i].scheme)
uc = curl_url_set(href, CURLUPART_SCHEME, tests[i].scheme, 0);
if(!uc && tests[i].host)
uc = curl_url_set(href, CURLUPART_HOST, tests[i].host, 0);
if(!uc && tests[i].port)
uc = curl_url_set(href, CURLUPART_PORT, tests[i].port, 0);
if(!uc && tests[i].path)
uc = curl_url_set(href, CURLUPART_PATH, tests[i].path, 0);
if(uc) {
curl_mfprintf(stderr, "failed to parse %d href %s://%s:%s%s -> %d\n",
i, tests[i].scheme, tests[i].host, tests[i].port,
tests[i].path, uc);
fails++;
goto loop_end;
}
match = Curl_url_same_origin(base, href);
if(match != tests[i].expect_match) {
curl_mfprintf(stderr, "ERROR: %d base %s and href %s://%s:%s%s %s\n",
i, tests[i].base, tests[i].scheme, tests[i].host,
tests[i].port, tests[i].path,
match ? "matched" : "did not match");
fails++;
}
loop_end:
curl_url_cleanup(base);
curl_url_cleanup(href);
}
abort_if(fails, "same_origin tests failed");
}
#endif /* !CURL_DISABLE_HTTP */
UNITTEST_END_SIMPLE
}