Compare commits

...

2 Commits

Author SHA1 Message Date
Vladimir Dubrovin
5babf687f6 proxy: credentials of an earlier request not inherited by the next
Some checks failed
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Has been cancelled
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-latest) (push) Has been cancelled
C/C++ CI MacOS / ${{ matrix.target }} (macos-15) (push) Has been cancelled
C/C++ CI Windows / ${{ matrix.target }} (windows-2022) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (macos-15) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-latest) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (windows-2022) (push) Has been cancelled
C/C++ CI cmake / ubuntu-latest (wolfSSL) (push) Has been cancelled
2026-11-13 17:38:47 +03:00
Vladimir Dubrovin
3894d25a02 httpsrv: credentials of an earlier request not inherited by the next 2026-11-13 17:14:36 +03:00
4 changed files with 73 additions and 0 deletions

View File

@ -1618,6 +1618,23 @@ void * httpsrvchild(struct clientparam *param)
r.lasthost = lasthost; r.lasthost = lasthost;
param->res = 0; param->res = 0;
/* What the request brought belongs to it alone. A connection
can carry a client's request after another's, as a reverse
proxy reuses its upstreams, so the credentials and the host
of an earlier request decide nothing for this one. */
if(param->username){
free(param->username);
param->username = NULL;
}
if(param->password){
free(param->password);
param->password = NULL;
}
if(param->hostname){
free(param->hostname);
param->hostname = NULL;
}
answered = httpsrv_request(param, &r); answered = httpsrv_request(param, &r);
handoff = r.handoff; handoff = r.handoff;
if(r.raw) free(r.raw); if(r.raw) free(r.raw);

View File

@ -340,6 +340,27 @@ for(;;){
clhdrofs_req = -1; clhdrofs_req = -1;
param->nolongdatfilter = 0; param->nolongdatfilter = 0;
/* What the request brought belongs to it alone. A connection can carry
a request after another's, so the credentials of an earlier request
decide nothing for this one. */
if(param->username){
free(param->username);
param->username = NULL;
}
if(param->password){
free(param->password);
param->password = NULL;
}
param->pwtype = 0;
if(param->extusername){
free(param->extusername);
param->extusername = NULL;
}
if(param->extpassword){
free(param->extpassword);
param->extpassword = NULL;
}
/* A connection waiting a while for its next request, or a tunnel, holds /* A connection waiting a while for its next request, or a tunnel, holds
no buffer: it is taken again once a request starts coming. */ no buffer: it is taken again once a request starts coming. */
if(buf && param->cliinbuf == param->clioffset){ if(buf && param->cliinbuf == param->clioffset){

View File

@ -1,5 +1,7 @@
"""Authentication and access rules in front of the HTTP server.""" """Authentication and access rules in front of the HTTP server."""
import base64
def run(t): def run(t):
if not t.skip_unless("httpsrv"): if not t.skip_unless("httpsrv"):
@ -49,3 +51,19 @@ def run(t):
t.contains(t.http(url + "/echo", auth=("alice", "secret")), "path=/echo", t.contains(t.http(url + "/echo", auth=("alice", "secret")), "path=/echo",
"an authenticated request is dispatched") "an authenticated request is dispatched")
# --- credentials do not outlive the request that carried them ------
# A kept connection answers a second request without credentials as
# unauthenticated: a reverse proxy reuses its upstreams between
# clients, so one client's login must not open another's way in.
creds = base64.b64encode(b"alice:secret").decode()
text, closed = t.raw_session(srv,
f"GET /echo HTTP/1.1\r\nHost: t\r\n"
f"Authorization: Basic {creds}\r\n\r\n"
f"GET /echo HTTP/1.1\r\nHost: t\r\nConnection: close\r\n\r\n")
parts = text.split("HTTP/1.")
t.eq(3, len(parts), "the kept connection answers both requests")
t.eq(True, parts[1].startswith("1 200"),
"the request with credentials is allowed")
t.eq(True, parts[2].startswith("0 401"),
"the request after it, without credentials, is refused")

View File

@ -4,6 +4,8 @@ Access rules accumulate until "flush", so each service section here starts
from a clean list. from a clean list.
""" """
import base64
def run(t): def run(t):
if not t.skip_unless("httpsrv"): if not t.skip_unless("httpsrv"):
@ -108,3 +110,18 @@ def run(t):
t.eq(407, t.http(origin + "/echo", proxy=needs_auth, t.eq(407, t.http(origin + "/echo", proxy=needs_auth,
proxy_auth=("alice", "wrong")).status, proxy_auth=("alice", "wrong")).status,
"wrong proxy credentials are refused") "wrong proxy credentials are refused")
# credentials do not outlive the request which carried them: a second
# request on the same connection, without them, is asked again
creds = base64.b64encode(b"alice:secret").decode()
text, closed = t.raw_session(auth,
f"GET {origin}/echo HTTP/1.1\r\nHost: 127.0.0.1:{srv}\r\n"
f"Proxy-Authorization: Basic {creds}\r\n\r\n"
f"GET {origin}/echo HTTP/1.1\r\nHost: 127.0.0.1:{srv}\r\n"
f"Connection: close\r\n\r\n")
parts = text.split("HTTP/1.")
t.eq(3, len(parts), "the kept connection answers both requests")
t.eq(True, parts[1].startswith("1 200"),
"the request with credentials is allowed")
t.eq(True, parts[2].startswith("0 407"),
"the request after it, without credentials, is refused")