mirror of
https://github.com/3proxy/3proxy.git
synced 2026-09-02 21:05:49 +08:00
Fix rewrite and over-long header handling on Windows
Some checks failed
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Waiting to run
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-latest) (push) Waiting to run
C/C++ CI MacOS / ${{ matrix.target }} (macos-15) (push) Waiting to run
C/C++ CI Windows / ${{ matrix.target }} (windows-2022) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (macos-15) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-latest) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (windows-2022) (push) Waiting to run
C/C++ CI cmake / ubuntu-latest (wolfSSL) (push) Waiting to run
Update HTML documentation / docs (push) Has been cancelled
Update wiki / wiki (push) Has been cancelled
Some checks failed
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Waiting to run
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-latest) (push) Waiting to run
C/C++ CI MacOS / ${{ matrix.target }} (macos-15) (push) Waiting to run
C/C++ CI Windows / ${{ matrix.target }} (windows-2022) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (macos-15) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-latest) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (windows-2022) (push) Waiting to run
C/C++ CI cmake / ubuntu-latest (wolfSSL) (push) Waiting to run
Update HTML documentation / docs (push) Has been cancelled
Update wiki / wiki (push) Has been cancelled
op_rewrite checked what it built with targetunsafe(), which describes a path on this machine: on Windows it must name a drive or a share, so a rewritten request path was refused and every rewrite rule failed there. A rewrite produces a request path and is checked as one. The header loop stopped at HTTPSRV_MAXHDR and answered anyway, leaving the rest of the request in the stream for the next one to be read out of. It refuses the request instead. A header longer than the buffer arrives as several lines, so the count bounds what is read rather than what a client may send in one header. The test for the buffer growth after a PCRE rewrite sent its request through to an httpsrv origin, which stops reading at that same cap; it uses an origin which reads whatever it is sent, since what is under test is the proxy in the middle.
This commit is contained in:
parent
5400d53cef
commit
f265ea0b52
@ -937,7 +937,11 @@ static int op_rewrite(struct httpreq *r, const unsigned char *params)
|
|||||||
if(!params || !*params) return op_forbidden(r);
|
if(!params || !*params) return op_forbidden(r);
|
||||||
if(expand(path, sizeof(path), params, r->path, r->caps, r->ncaps))
|
if(expand(path, sizeof(path), params, r->path, r->caps, r->ncaps))
|
||||||
return op_forbidden(r);
|
return op_forbidden(r);
|
||||||
if(targetunsafe(path) || path[0] != '/') return op_forbidden(r);
|
/* What comes out is a request path and is checked as one. A path on this
|
||||||
|
machine is a different thing, and asking a rewrite to look like one
|
||||||
|
would leave nothing writable on Windows, where such a path names a
|
||||||
|
drive or a share. */
|
||||||
|
if(path[0] != '/' || pathunsafe(path)) return op_forbidden(r);
|
||||||
strcpy(r->path, path);
|
strcpy(r->path, path);
|
||||||
return HTTPSRV_REWRITTEN;
|
return HTTPSRV_REWRITTEN;
|
||||||
}
|
}
|
||||||
@ -1327,9 +1331,12 @@ static int httpsrv_request(struct clientparam *param, struct httpreq *r)
|
|||||||
strcpy(r->path, decoded);
|
strcpy(r->path, decoded);
|
||||||
}
|
}
|
||||||
|
|
||||||
while(hdrs++ < HTTPSRV_MAXHDR &&
|
/* A header longer than the buffer arrives as several lines, so the count
|
||||||
|
bounds what is read and not what a client may send in one header. */
|
||||||
|
while(hdrs < HTTPSRV_MAXHDR &&
|
||||||
(i = sockgetlinebuf(param, CLIENT, (unsigned char *)buf, sizeof(buf) - 1,
|
(i = sockgetlinebuf(param, CLIENT, (unsigned char *)buf, sizeof(buf) - 1,
|
||||||
'\n', conf.timeouts[STRING_S])) > 2){
|
'\n', conf.timeouts[STRING_S])) > 2){
|
||||||
|
hdrs++;
|
||||||
if(rawkeep(r, buf, i)) RETURN(710);
|
if(rawkeep(r, buf, i)) RETURN(710);
|
||||||
buf[i] = 0;
|
buf[i] = 0;
|
||||||
if(!strncasecmp(buf, "host:", 5) && !r->proxy){
|
if(!strncasecmp(buf, "host:", 5) && !r->proxy){
|
||||||
@ -1384,6 +1391,11 @@ static int httpsrv_request(struct clientparam *param, struct httpreq *r)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The headers ran past what this server reads, so the rest of the request
|
||||||
|
is still in the stream and there is no answering it: what follows would
|
||||||
|
be read as the request after this one. */
|
||||||
|
if(hdrs >= HTTPSRV_MAXHDR) RETURN(712);
|
||||||
|
|
||||||
/* The next request begins where this body ends, so a body which cannot
|
/* The next request begins where this body ends, so a body which cannot
|
||||||
be read to its end - one this server does not frame, or one longer
|
be read to its end - one this server does not frame, or one longer
|
||||||
than it is willing to read - closes the connection instead.
|
than it is willing to read - closes the connection instead.
|
||||||
|
|||||||
@ -198,14 +198,24 @@ def run(t):
|
|||||||
# GHSA-h845-prxq-ww3q: a rewrite that doubles the client headers used to
|
# GHSA-h845-prxq-ww3q: a rewrite that doubles the client headers used to
|
||||||
# leave a buffer holding exactly what it produced, and the Content-Length
|
# leave a buffer holding exactly what it produced, and the Content-Length
|
||||||
# the data filter regenerates was then written past the end of it.
|
# the data filter regenerates was then written past the end of it.
|
||||||
|
# The origin here reads whatever it is sent and answers the same way every
|
||||||
|
# time: what is being tested is the proxy in the middle, not what a server
|
||||||
|
# is willing to accept in one request.
|
||||||
|
grown = t.free_port()
|
||||||
|
stop = t.raw_server(grown, b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok",
|
||||||
|
drain=True)
|
||||||
|
try:
|
||||||
p = proxy_with("rewrite_grow",
|
p = proxy_with("rewrite_grow",
|
||||||
'pcre_rewrite cliheader dunno "(?s).*" "$0$0"',
|
'pcre_rewrite cliheader dunno "(?s).*" "$0$0"',
|
||||||
'pcre clidata dunno *')
|
'pcre clidata dunno *')
|
||||||
big = "".join("X-%d: %s\r\n" % (i, chr(65 + i) * 20000) for i in range(5))
|
big = "".join("X-%d: %s\r\n" % (i, chr(65 + i) * 20000) for i in range(5))
|
||||||
reply = t.raw_proxy_request(p, url + "/echo", extra=big, body="z")
|
reply = t.raw_proxy_request(p, f"http://127.0.0.1:{grown}/x",
|
||||||
|
extra=big, body="z")
|
||||||
t.contains(reply, "200", "a doubled header block with a body is answered")
|
t.contains(reply, "200", "a doubled header block with a body is answered")
|
||||||
t.contains(t.http(url + "/echo", proxy=p), "path=/echo",
|
t.contains(t.raw_proxy_request(p, f"http://127.0.0.1:{grown}/x"), "200",
|
||||||
"and the proxy is still there afterwards")
|
"and the proxy is still there afterwards")
|
||||||
|
finally:
|
||||||
|
stop()
|
||||||
|
|
||||||
# A reference to a group the pattern does not have is dropped, and dropped
|
# A reference to a group the pattern does not have is dropped, and dropped
|
||||||
# by both the pass which measures the result and the pass which writes it.
|
# by both the pass which measures the result and the pass which writes it.
|
||||||
|
|||||||
@ -375,12 +375,14 @@ class Tester:
|
|||||||
text, _ = self.raw_session(pport, request, host=phost, quiet=2)
|
text, _ = self.raw_session(pport, request, host=phost, quiet=2)
|
||||||
return text
|
return text
|
||||||
|
|
||||||
def raw_server(self, port, reply, close_after=True, host="127.0.0.1"):
|
def raw_server(self, port, reply, close_after=True, host="127.0.0.1",
|
||||||
|
drain=False):
|
||||||
"""Answer every connection with fixed bytes. Returns a stop function.
|
"""Answer every connection with fixed bytes. Returns a stop function.
|
||||||
|
|
||||||
For the shapes a real server would have to be talked into: an answer
|
For the shapes a real server would have to be talked into: an answer
|
||||||
whose body is delimited by the close, or one which promises to stay
|
whose body is delimited by the close, or one which promises to stay
|
||||||
and does not.
|
and does not. drain reads the whole request first, however large,
|
||||||
|
which is what a test of the sending side needs.
|
||||||
"""
|
"""
|
||||||
sock = socket.socket()
|
sock = socket.socket()
|
||||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
@ -395,8 +397,13 @@ class Tester:
|
|||||||
except OSError:
|
except OSError:
|
||||||
break
|
break
|
||||||
try:
|
try:
|
||||||
conn.settimeout(self.timeout)
|
conn.settimeout(0.5 if drain else self.timeout)
|
||||||
conn.recv(65536)
|
while True:
|
||||||
|
try:
|
||||||
|
if not conn.recv(65536) or not drain:
|
||||||
|
break
|
||||||
|
except socket.timeout:
|
||||||
|
break # it has stopped sending
|
||||||
conn.sendall(reply)
|
conn.sendall(reply)
|
||||||
if close_after:
|
if close_after:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user