mirror of
https://gitlab.com/openconnect/ocserv.git
synced 2026-08-08 09:21:48 +08:00
When two HTTP requests arrived in the same TLS read buffer, a single llhttp_execute() call would fire callbacks for both requests inline. Because http_req_reset() is not called between them, ws->req ended up reflecting the second request's URL and headers, silently discarding the first. In the worst case, body bytes from the first request accumulated alongside the second request's body. Fix this by registering an on_message_begin callback that returns HPE_PAUSED when an existing message is detected. Resolves: #716 Signed-off-by: Nikos Mavrogiannopoulos <n.mavrogiannopoulos@gmail.com>
112 lines
4.6 KiB
Bash
Executable File
112 lines
4.6 KiB
Bash
Executable File
#!/bin/sh
|
|
#
|
|
# Copyright (C) 2026 Nikos Mavrogiannopoulos
|
|
#
|
|
# This file is part of ocserv.
|
|
#
|
|
# ocserv 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 2 of the License, or (at
|
|
# your option) any later version.
|
|
#
|
|
# ocserv 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 GnuTLS; if not, write to the Free Software Foundation,
|
|
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
# Verify that HTTP request-boundary confusion is not possible.
|
|
# When two requests arrive in the same TLS read buffer, the parser must
|
|
# stop at the first message_complete boundary (via llhttp_pause) and
|
|
# dispatch only the first request. The second request must not reach
|
|
# any handler in that same keep-alive cycle.
|
|
|
|
SERV="${SERV:-../src/ocserv}"
|
|
srcdir=${srcdir:-.}
|
|
NO_NEED_ROOT=1
|
|
|
|
. `dirname $0`/common.sh
|
|
|
|
eval "${GETPORT}"
|
|
|
|
if ! which gnutls-cli > /dev/null 2>&1; then
|
|
echo "gnutls-cli not found, needed for this test"
|
|
exit 1
|
|
fi
|
|
|
|
echo "Testing HTTP request smuggling prevention..."
|
|
|
|
update_config test-user-cert.config
|
|
launch_simple_sr_server -d 1 -f -c ${CONFIG}
|
|
PID=$!
|
|
wait_server $PID
|
|
|
|
TMPFILE=test-http-smuggling.$$.tmp
|
|
|
|
# Send raw bytes over TLS via gnutls-cli and capture the full server
|
|
# response. Because stdin is a pipe (not a terminal) gnutls-cli delivers
|
|
# all data to gnutls_record_send() in a single call, placing both requests
|
|
# in the same TLS record — the exact scenario that triggers the bug.
|
|
# tls_send takes a printf(1) format string with \r\n escape sequences and
|
|
# sends the result over TLS via gnutls-cli. The argument is intentionally
|
|
# NOT passed through $() to avoid shell command substitution stripping
|
|
# trailing newlines, which would drop the blank line that terminates HTTP
|
|
# headers.
|
|
#
|
|
# The "sleep 1" keeps the pipe open after the request bytes are sent so
|
|
# that gnutls-cli stays in its select loop long enough to receive the
|
|
# server response before stdin EOF triggers a TLS close_notify.
|
|
tls_send() {
|
|
{ printf '%b' "$1"; sleep 1; } | \
|
|
LD_PRELOAD=libsocket_wrapper.so timeout 5 \
|
|
gnutls-cli \
|
|
--insecure --x509cafile="${srcdir}/certs/ca.pem" \
|
|
$ADDRESS --port $PORT \
|
|
--sni-hostname localhost > "$TMPFILE" || true
|
|
}
|
|
|
|
# --- Positive: single GET must still return a response ------------------
|
|
echo -n "Single GET /cert.pem returns a response ... "
|
|
tls_send 'GET /cert.pem HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n'
|
|
grep -q "^HTTP/1\." "$TMPFILE" ||
|
|
fail $PID "Single GET /cert.pem produced no HTTP response"
|
|
echo "ok"
|
|
|
|
# --- Negative: second pipelined request must not be dispatched ----------
|
|
# Strategy: second URL is /cert.pem (→ 200). Before the fix the second
|
|
# request's URL overwrote the first and the server returned 200. After
|
|
# the fix the server detects pipelined data and closes the connection
|
|
# immediately — neither request gets a response, but /cert.pem is never
|
|
# served.
|
|
echo -n "Pipelined GET: /cert.pem not served ... "
|
|
tls_send 'GET /BOGUS_SENTINEL HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\nGET /cert.pem HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n'
|
|
if grep -q "^HTTP/1.1 200" "$TMPFILE"; then
|
|
fail $PID "Second pipelined request was dispatched: got 200 for /cert.pem"
|
|
fi
|
|
echo "ok"
|
|
|
|
# --- Negative: GET with body bytes + pipelined GET ----------------------
|
|
# The body bytes of the first GET must not bleed into the second request's
|
|
# URL parsing.
|
|
echo -n "GET with body + pipelined GET: first request dispatched ... "
|
|
tls_send 'GET /BOGUS_SENTINEL HTTP/1.1\r\nHost: localhost\r\nContent-Length: 5\r\nConnection: keep-alive\r\n\r\nabcdeGET /cert.pem HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n'
|
|
if grep -q "^HTTP/1.1 200" "$TMPFILE"; then
|
|
fail $PID "Pipelined request after GET body was dispatched: expected 404, got 200 for /cert.pem"
|
|
fi
|
|
echo "ok"
|
|
|
|
# --- Negative: GET with Content-Length: 0 + pipelined GET ---------------
|
|
echo -n "GET Content-Length:0 + pipelined GET: first request dispatched ... "
|
|
tls_send 'GET /BOGUS_SENTINEL HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\nConnection: keep-alive\r\n\r\nGET /cert.pem HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n'
|
|
if grep -q "^HTTP/1.1 200" "$TMPFILE"; then
|
|
fail $PID "Pipelined request after CL:0 GET was dispatched: expected 404, got 200 for /cert.pem"
|
|
fi
|
|
echo "ok"
|
|
|
|
rm -f "$TMPFILE"
|
|
cleanup
|
|
exit 0
|