[
  {
    "id": "CL-SEC-2026-0001",
    "title": "Gemini API key transmitted as URL query parameter",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "high",
    "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
    "cvss-score": "7.5",
    "cwe": "CWE-598",
    "project-name": "cl-completions",
    "homepage": "https://github.com/atgreen/cl-completions",
    "introduced-commit": "649ee5d",
    "introduced-url": "https://github.com/atgreen/cl-completions/commit/649ee5d",
    "fixed-commit": "b63ba8e",
    "fixed-version": "1.1.0",
    "fixed-url": "https://github.com/atgreen/cl-completions/commit/b63ba8e",
    "cve-id": null,
    "status": "draft",
    "description": "The Gemini provider in cl-completions constructs its endpoint URL by\nappending the API key as a query parameter:\n\n    (format nil \"...generateContent?key=~A\" api-key)\n\nThis embeds the secret key directly in the URL string.  The key is\nthen exposed through multiple channels:\n\n  - HTTP proxy and server access logs, which routinely record full\n    request URIs including query strings.\n  - Debug output when *debug-stream* is bound, since the endpoint URL\n    is logged as part of request tracing.\n  - Error messages generated by safe-http-request (see CL-SEC-2026-0002),\n    which include the full request URI in the signalled condition.\n  - Any network-level monitoring or packet capture that inspects URL\n    strings.\n\nThe OpenAI and Anthropic providers in the same library correctly\ntransmit credentials via HTTP headers, demonstrating that the\nheader-based approach is already established in this codebase.\n\nGoogle's Gemini API supports the x-goog-api-key header as an\nalternative to the query parameter.",
    "recommendation": "Move the API key from the URL query parameter to the x-goog-api-key\nHTTP header:\n\n    (headers `((\"Content-Type\" . \"application/json\")\n               (\"x-goog-api-key\" . ,api-key)))\n\n    (endpoint (format nil\n                \"https://generativelanguage.googleapis.com/v1beta/models/~A:~A\"\n                model\n                (if use-streaming \"streamGenerateContent?alt=sse\"\n                                  \"generateContent\")))\n\nThis is consistent with how the OpenAI and Anthropic providers in the\nsame library already handle credentials."
  },
  {
    "id": "CL-SEC-2026-0002",
    "title": "Full request URI including credentials leaked in error messages",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "medium",
    "cvss": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N",
    "cvss-score": "5.5",
    "cwe": "CWE-209",
    "project-name": "cl-completions",
    "homepage": "https://github.com/atgreen/cl-completions",
    "introduced-commit": "392c08c",
    "introduced-url": "https://github.com/atgreen/cl-completions/commit/392c08c",
    "fixed-commit": "b63ba8e",
    "fixed-version": "1.1.0",
    "fixed-url": "https://github.com/atgreen/cl-completions/commit/b63ba8e",
    "cve-id": null,
    "status": "draft",
    "description": "The safe-http-request function in cl-completions signals an error\ncondition that includes the full request URI obtained via\ndex:request-uri:\n\n    (error \"An HTTP request to ~S has failed (status=~A).~%~%~A\"\n           (dex:request-uri e)\n           (dex:response-status e)\n           string-body)\n\nWhen combined with CL-SEC-2026-0001 (Gemini API key in URL), this\nleaks the API key into any error handler, log collector, or\ncondition-reporting mechanism that captures the error text.\n\nEven without CL-SEC-2026-0001, this is independently a concern:\nendpoint URLs may contain session tokens, internal hostnames, or\npath-based routing information that should not appear in error output\nvisible to end users or forwarded to external error-tracking services.\n\nThis vulnerability predates the Gemini provider \u2014 it was introduced\nwhen the error-handling wrapper was first added in 392c08c, affecting\nall providers.",
    "recommendation": "Strip query parameters from the URI before including it in error\nmessages:\n\n    (let* ((uri (princ-to-string (dex:request-uri e)))\n           (qpos (position #\\? uri))\n           (safe-uri (if qpos (subseq uri 0 qpos) uri)))\n      (error \"An HTTP request to ~S has failed (status=~A).~%~%~A\"\n             safe-uri\n             (dex:response-status e)\n             string-body))\n\nAlternatively, omit the URI entirely from the error message and log\nit separately at a debug level that is not captured in production."
  },
  {
    "id": "CL-SEC-2026-0003",
    "title": "TLS certificate verification disabled by default in gRPC client",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "high",
    "cvss": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N",
    "cvss-score": "7.4",
    "cwe": "CWE-295",
    "project-name": "ag-gRPC",
    "homepage": "https://github.com/atgreen/ag-gRPC",
    "introduced-commit": "9af1bd8",
    "introduced-url": "https://github.com/atgreen/ag-gRPC/commit/9af1bd8",
    "fixed-commit": "c234e75",
    "fixed-version": "1.3.0",
    "fixed-url": "https://github.com/atgreen/ag-gRPC/commit/c234e75",
    "cve-id": null,
    "status": "draft",
    "description": "Both make-channel (with :tls t) and make-secure-channel default to\n:tls-verify nil / :verify nil, meaning TLS connections do not\nvalidate the server's certificate by default.\n\nIn channel.lisp:\n\n    (defun make-channel (host port &key ... tls (tls-verify nil) ...)\n\n    (defun make-secure-channel (host port &key ... (verify nil) ...)\n\nThis is forwarded to wrap-stream-with-tls in tls.lisp, which passes\n+VERIFY-NONE+ to pure-tls when verify is nil.\n\nA caller using make-secure-channel reasonably expects a secure\nconnection, but gets one that is vulnerable to man-in-the-middle\ninterception.  An attacker who can intercept the TCP connection\n(via DNS hijacking, ARP poisoning, BGP hijacking, or a compromised\nnetwork intermediary) can present their own certificate and proxy\nall traffic in both directions.\n\nThis default is unsafe on its own terms: enabling TLS while\ndisabling certificate verification defeats the authentication\nproperty that users normally expect from a secure channel.  This\nalso diverges from the usual expectation for TLS client APIs,\nwhich normally verify the peer certificate by default unless the\ncaller explicitly opts out.",
    "recommendation": "Change the default for tls-verify / verify to T when TLS is enabled:\n\n    (defun make-channel (host port &key ... tls (tls-verify tls) ...)\n\n    (defun make-secure-channel (host port &key ... (verify t) ...)\n\nCallers who need to disable verification (e.g. for development with\nself-signed certificates) can still pass :tls-verify nil or\n:verify nil explicitly."
  },
  {
    "id": "CL-SEC-2026-0004",
    "title": "Authentication credentials forwarded on cross-origin HTTP redirect",
    "reported": "2026-03-31",
    "published": "2026-04-07",
    "severity": "high",
    "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:N/A:N",
    "cvss-score": "7.4",
    "cwe": "CWE-522",
    "project-name": "dexador",
    "homepage": "https://github.com/fukamachi/dexador",
    "introduced-commit": "f0e47ab",
    "introduced-url": "https://github.com/fukamachi/dexador/commit/f0e47ab",
    "fixed-commit": "5820f60",
    "fixed-version": "0.9.16",
    "fixed-url": "https://github.com/fukamachi/dexador/commit/5820f60dc8a351bbc424b14da5e9bccd44b2933e",
    "cve-id": null,
    "status": "fixed",
    "audit-verdict": "reviewed",
    "audit-notes": "Real bug, confirmed in both usocket and winhttp backends. Requires redirect following plus malicious origin or open redirect, keeping it from unconditional credential leak.",
    "description": "When dexador follows a 301, 302, 303, 307, or 308 redirect to a\ndifferent host, it forwards the original request's authentication\ncredentials to the new target host.\n\nIn the usocket backend (src/backend/usocket.lisp), the cross-host\nredirect path (line 743-759) passes the full &rest args plist to the\nrecursive request call.  Only :headers, :max-redirects, :method, and\n:stream are modified; :basic-auth and :bearer-auth are carried through\nunchanged.  The :headers list is also preserved via nconc, so any\nexplicit Authorization header supplied by the caller is forwarded as\nwell.\n\nIn the winhttp backend (src/backend/winhttp.lisp, line 224-228), the\nentire args plist is passed unmodified to the recursive call, producing\nthe same behavior.\n\nAn attacker who controls the initial target (or who can inject a\nredirect via an open-redirect vulnerability on a trusted host) can\nredirect the request to a host they control and capture the victim's\ncredentials:\n\n    ;; User trusts example.com\n    (dex:get \"https://example.com/api\"\n             :bearer-auth \"ghp_secret_token\")\n\n    ;; example.com has an open redirect, or the attacker controls\n    ;; the initial host.  302 Location: https://evil.com/steal\n    ;; => dexador sends \"Authorization: Bearer ghp_secret_token\"\n    ;;    to evil.com\n\nCookie-jar cookies are NOT affected: build-cookie-headers is called\nwith the new URI on same-host redirects, and on cross-host redirects\nthe cookie-jar is re-evaluated per domain by the recursive request.\n\nThis is the same class of vulnerability as:\n  - Python requests: CVE-2023-32681, CVE-2018-18074\n  - Go net/http: (stripped by default since Go 1.0)\n  - curl: CVE-2018-1000007 (fixed in 7.58.0)\n  - ruby net/http: CVE-2024-25126",
    "recommendation": "Before the recursive request call for cross-host redirects, strip\nauthentication credentials from the args plist:\n\n  (remf args :basic-auth)\n  (remf args :bearer-auth)\n\nAnd remove any Authorization entry from the :headers alist:\n\n  (setf (getf args :headers)\n        (remove :authorization (getf args :headers)\n                :key #'car :test #'string-equal))\n\nApply the same fix to both the usocket and winhttp backends.\n\nUsers who need to send credentials to the redirect target can use a\ncookie-jar (which already respects domain scoping) or handle redirects\nmanually via :max-redirects 0."
  },
  {
    "id": "CL-SEC-2026-0019",
    "title": "Session fixation via client-supplied session ID reuse",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "high",
    "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N",
    "cvss-score": "8.1",
    "cwe": "CWE-384",
    "project-name": "lack",
    "homepage": "https://github.com/fukamachi/lack",
    "introduced-commit": "f3302ce",
    "introduced-url": "https://github.com/fukamachi/lack/commit/f3302ce",
    "fixed-commit": "c5c1e94",
    "fixed-version": "0.3.0",
    "fixed-url": "https://github.com/fukamachi/lack/commit/c5c1e94",
    "cve-id": "GHSA-mj27-4cqf-vf3r",
    "status": "published",
    "description": "The session middleware (src/middleware/session.lisp lines 26-32)\naccepts client-supplied session IDs even when no corresponding\nsession exists in the store:\n\n    (let* ((sid (extract-sid state env))\n           (session (and sid (fetch-session store sid)))\n           (sid (or sid (generate-sid state env)))\n           ...)\n\nWhen a client supplies a session ID via cookie but no session data\nexists in the store for that ID (fetch-session returns nil), the\nserver still uses the client-supplied sid (line 29: the or form\nreturns sid because it is non-nil, so generate-sid is never called).\nThe session is then stored under this attacker-controlled ID.\n\nAn attacker can exploit this for session fixation:\n\n  1. Generate a valid-format session ID (40 hex chars, matching the\n     validator regex in state/cookie.lisp)\n  2. Inject it into a victim's browser via a subdomain cookie, XSS,\n     or link with cookie-setting redirect\n  3. Wait for the victim to authenticate\n  4. Use the known session ID to access the victim's authenticated\n     session",
    "recommendation": "Always generate a new server-side session ID when no existing session\nis found in the store, regardless of what the client supplied:\n\n    (let* ((sid (extract-sid state env))\n           (session (and sid (fetch-session store sid)))\n           (sid (if session sid (generate-sid state env)))\n           ...)\n\nThis ensures that client-supplied IDs are only honored when they\ncorrespond to an existing server-side session."
  },
  {
    "id": "CL-SEC-2026-0020",
    "title": "Unsafe deserialization in session stores escalates data injection to code execution",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "high",
    "cvss": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H",
    "cvss-score": "8.1",
    "cwe": "CWE-502",
    "project-name": "lack",
    "homepage": "https://github.com/fukamachi/lack",
    "introduced-commit": "f3302ce",
    "introduced-url": "https://github.com/fukamachi/lack/commit/f3302ce",
    "fixed-commit": "df23ab1",
    "fixed-version": "0.3.0",
    "fixed-url": "https://github.com/fukamachi/lack/commit/df23ab1",
    "cve-id": "GHSA-9rrq-wr26-x46h",
    "status": "published",
    "description": "Both the DBI and Redis session stores use read-from-string as part\nof their default deserializer:\n\nIn src/middleware/session/store/redis.lisp line 40:\n\n    (deserializer (lambda (data)\n                    (unmarshal (read-from-string\n                                (utf-8-bytes-to-string\n                                 (base64-string-to-usb8-array data))))))\n\nIn src/middleware/session/store/dbi.lisp line 38, the identical\npattern.\n\nread-from-string is the full Common Lisp reader, which by default\nprocesses reader macros including #. (read-time evaluation).  If\n*read-eval* is not explicitly bound to nil before calling\nread-from-string, an attacker who can write to the session store\n(via SQL injection elsewhere, direct Redis access, a shared Redis\ninstance, or any other data injection vector) can inject a payload\nsuch as:\n\n    #.(uiop:run-program \"id\" :output :string)\n\ninto the serialized session data, achieving arbitrary code execution\nwhen the session is loaded on the next request.\n\nThe base64 encoding does not prevent exploitation -- the attacker\nsimply base64-encodes the malicious payload.",
    "recommendation": "Bind *read-eval* to nil before calling read-from-string:\n\n    (deserializer (lambda (data)\n                    (let ((*read-eval* nil))\n                      (unmarshal (read-from-string\n                                  (utf-8-bytes-to-string\n                                   (base64-string-to-usb8-array data)))))))\n\nBetter yet, replace read-from-string with a safe deserialization\nformat (e.g., JSON or a purpose-built binary format) that does not\ninvoke the Lisp reader at all."
  },
  {
    "id": "CL-SEC-2026-0045",
    "title": "No domain validation at cookie storage time enables cookie jar poisoning",
    "reported": "2026-03-31",
    "published": "2026-04-07",
    "severity": "medium",
    "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N",
    "cvss-score": "6.5",
    "cwe": "CWE-565",
    "project-name": "cl-cookie",
    "homepage": "https://github.com/fukamachi/cl-cookie",
    "introduced-commit": "8a2fb71",
    "introduced-url": "https://github.com/fukamachi/cl-cookie/commit/8a2fb71",
    "fixed-commit": "355f9c1",
    "fixed-version": "v0.2.0",
    "fixed-url": "https://github.com/fukamachi/cl-cookie/commit/355f9c1",
    "cve-id": null,
    "status": "fixed",
    "description": "When parse-set-cookie-header parses a Set-Cookie response, it\nstores the domain attribute verbatim (src/cl-cookie.lisp line 392)\nwithout checking whether the origin server is authorized to set\ncookies for that domain.\n\nPer RFC 6265 Section 5.3 step 5, a cookie's domain attribute MUST\nbe rejected if the origin host does not domain-match it.\n\nDomain validation via cookie-domain-p only happens at cookie\nretrieval time (match-cookie, line 194), not at storage time.\nThis means:\n\n  1. A malicious server at evil.com can set Domain=victim.com on\n     a cookie.\n  2. The cookie is accepted and stored in the cookie jar.\n  3. merge-cookies' delete-duplicates (line 248) can overwrite a\n     legitimate cookie for the same name/path/domain combination.\n\nIf a user visits evil.com before victim.com, evil.com can\noverwrite the victim's session cookie in the jar.  When the user\nvisits victim.com, the legitimate cookie has been destroyed.\nThis enables session fixation and denial of service.",
    "recommendation": "In parse-set-cookie-header, after parsing the domain attribute,\nverify that origin-host domain-matches the cookie domain per\nRFC 6265 Section 5.3 step 5.  Reject or ignore the domain\nattribute if validation fails."
  },
  {
    "id": "CL-SEC-2026-0051",
    "title": "HTTP request smuggling via duplicate Content-Length headers (last wins)",
    "reported": "2026-03-31",
    "published": "2026-04-07",
    "severity": "high",
    "cvss": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N",
    "cvss-score": "7.4",
    "cwe": "CWE-444",
    "project-name": "fast-http",
    "homepage": "https://github.com/fukamachi/fast-http",
    "introduced-commit": "2232fc9",
    "introduced-url": "https://github.com/fukamachi/fast-http/commit/2232fc9",
    "fixed-version": "v0.3.0",
    "fixed-commit": "1eb08b3",
    "fixed-url": "https://github.com/fukamachi/fast-http/commit/1eb08b3191c4edfa867dd294f6f38dcd90b4f13f",
    "cve-id": null,
    "status": "fixed",
    "description": "When a second Content-Length header is encountered,\nparse-header-field-and-value (src/parser.lisp line 314)\nunconditionally overwrites the previously stored value:\n\n    (setf (http-content-length http) content-length)\n\nThere is no check for whether http-content-length has already been\nset.  Per RFC 9112 Section 6.3, a message with multiple\nContent-Length values that disagree MUST be rejected as invalid.\n\nBy silently accepting the last value, fast-http allows a front-end\nproxy that uses the first Content-Length to disagree with fast-http\n(which uses the last), enabling classic CL-CL request smuggling.\n\nAdditionally, the higher-level collect-prev-header-value function\n(fast-http.lisp lines 129-135) concatenates duplicate header values\nwith \", \" -- but this happens after the parser has already set\nhttp-content-length to the second value.  The http struct uses the\nlast CL value while the headers hash table presents \"100, 50\" --\nan inconsistency that further enables smuggling.",
    "recommendation": "Reject requests with duplicate Content-Length headers that have\ndifferent values.  If values are identical, accept the single\nvalue.  If values differ, return 400 Bad Request."
  },
  {
    "id": "CL-SEC-2026-0052",
    "title": "HTTP request smuggling via Transfer-Encoding / Content-Length conflict not rejected",
    "reported": "2026-03-31",
    "published": "2026-04-07",
    "severity": "high",
    "cvss": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N",
    "cvss-score": "7.4",
    "cwe": "CWE-444",
    "project-name": "fast-http",
    "homepage": "https://github.com/fukamachi/fast-http",
    "introduced-commit": "2232fc9",
    "introduced-url": "https://github.com/fukamachi/fast-http/commit/2232fc9",
    "fixed-version": "v0.3.0",
    "fixed-commit": "cfd9161",
    "fixed-url": "https://github.com/fukamachi/fast-http/commit/cfd9161c90944df9c796ced26dada656c4c7e560",
    "cve-id": null,
    "status": "fixed",
    "description": "When a request contains both Transfer-Encoding: chunked and a\nContent-Length header, fast-http accepts both without error.  The\nparser decides body framing based on the http-chunked-p flag\n(src/parser.lisp lines 717-720):\n\n    (setf (http-state http)\n          (if (http-chunked-p http)\n              +state-chunk-size+\n              +state-body+))\n\nPer RFC 9112 Section 6.3: \"If a message is received with both a\nTransfer-Encoding and a Content-Length header field, the\nTransfer-Encoding overrides the Content-Length.  Such a message\nmight indicate an attempt to perform request smuggling and ought\nto be handled as an error.\"\n\nfast-http does NOT reject this as an error and does NOT clear the\nContent-Length when Transfer-Encoding is present.  A proxy that\nroutes based on Content-Length while fast-http uses chunked framing\ncreates a classic TE-CL smuggling vector.\n\nThis is the standard deployment model for Woo (the CL web server\nthat uses fast-http), which is typically placed behind nginx or\nanother reverse proxy.",
    "recommendation": "Reject requests that contain both Transfer-Encoding and\nContent-Length headers with a 400 Bad Request response.\n\nIf backward compatibility requires accepting such requests,\nat minimum clear http-content-length when Transfer-Encoding:\nchunked is detected."
  },
  {
    "id": "CL-SEC-2026-0053",
    "title": "Transfer-Encoding obfuscation via trailing whitespace enables TE-TE request smuggling",
    "reported": "2026-03-31",
    "published": "2026-04-07",
    "severity": "medium",
    "cvss": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:N",
    "cvss-score": "5.9",
    "cwe": "CWE-444",
    "project-name": "fast-http",
    "homepage": "https://github.com/fukamachi/fast-http",
    "introduced-commit": "2232fc9",
    "introduced-url": "https://github.com/fukamachi/fast-http/commit/2232fc9",
    "fixed-version": "v0.3.0",
    "fixed-commit": "e86789a",
    "fixed-url": "https://github.com/fukamachi/fast-http/commit/e86789a6bebf7f2015eda8ee7146b21ed3fbf03c",
    "cve-id": null,
    "status": "fixed",
    "audit-verdict": "reviewed",
    "audit-notes": "General parser-differential class is credible. Some specific obfuscation examples may depend on intermediary behavior not shown.",
    "description": "The Transfer-Encoding parser uses match-i-case for case-insensitive\nmatching of \"chunked\" (src/parser.lisp lines 353-377).  If the\nvalue does not exactly match after trimming leading whitespace,\nchunkedp is set to nil.\n\nObfuscated variants that some proxies accept as chunked are treated\nas non-chunked by fast-http:\n\n  - \"chunked \" (trailing space): fast-http sets chunkedp = nil\n  - \"chunked\\x00\" (null byte): treated as not-chunked\n  - \"chunked, identity\": treated as not-chunked\n\nThe trailing-space case is particularly dangerous: fast-http sets\nchunkedp = nil while a front-end proxy that trims whitespace\nbefore comparison sees \"chunked\", creating a TE-TE desync.\n\nAdditionally, duplicate Transfer-Encoding headers are silently\naccepted with the last value winning (line 324), enabling:\n\n    Transfer-Encoding: chunked\n    Transfer-Encoding: identity\n\nThe second header overrides chunkedp to nil.  A proxy using the\nfirst value sees chunked; fast-http sees identity.",
    "recommendation": "Trim whitespace from the Transfer-Encoding value before comparison.\nReject requests with multiple conflicting Transfer-Encoding headers.\nReject requests with unrecognized Transfer-Encoding values."
  },
  {
    "id": "CL-SEC-2026-0054",
    "title": "Unbounded chunk size hex parsing enables memory and CPU exhaustion",
    "reported": "2026-03-31",
    "published": "2026-04-07",
    "severity": "high",
    "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
    "cvss-score": "7.5",
    "cwe": "CWE-400",
    "project-name": "fast-http",
    "homepage": "https://github.com/fukamachi/fast-http",
    "introduced-commit": "2232fc9",
    "introduced-url": "https://github.com/fukamachi/fast-http/commit/2232fc9",
    "fixed-version": "v0.3.0",
    "fixed-commit": "025dad5",
    "fixed-url": "https://github.com/fukamachi/fast-http/commit/025dad5dc41bd58755fe3f3c05c73e04876341ac",
    "cve-id": null,
    "status": "fixed",
    "description": "The chunk size parser (src/parser.lisp lines 556-591) accumulates\nhex digits into http-content-length without any upper bound check:\n\n    (setf (http-content-length http)\n          (+ (* 16 (http-content-length http)) unhex-val))\n\nThe type is arbitrary-precision integer (CL bignum).  A malicious\nclient can send a chunk size with thousands of hex digits\n(e.g., FFFFFF...FFFFFF\\r\\n), causing:\n\n  1. Memory exhaustion via unbounded bignum allocation\n  2. CPU exhaustion via progressively slower bignum multiplication\n  3. Downstream logic errors when the astronomical content-length\n     is compared against fixnum values",
    "recommendation": "Limit chunk size hex digits to a reasonable maximum (e.g., 16\ndigits for a 64-bit value).  Reject chunks with sizes that exceed\na configurable maximum (e.g., 1GB)."
  },
  {
    "id": "CL-SEC-2026-0112",
    "title": "Missing TLS 1.3 downgrade sentinel check in pure-tls client handshake",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "medium",
    "cvss": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:N",
    "cvss-score": "6.8",
    "cwe": "CWE-757",
    "project-name": "pure-tls",
    "homepage": "https://github.com/atgreen/pure-tls",
    "introduced-commit": "a3076f0",
    "introduced-url": "https://github.com/atgreen/pure-tls/commit/a3076f0",
    "fixed-commit": "3d2db0d",
    "fixed-version": "1.11.0",
    "fixed-url": "https://github.com/atgreen/pure-tls/commit/3d2db0d",
    "audit-verdict": "severity-adjusted",
    "audit-notes": "pure-tls only supports TLS 1.3 and rejects non-1.3 negotiation, which substantially limits the practical downgrade scenario. Protocol-compliance and defense-in-depth, not strong standalone high-severity.",
    "description": "RFC 8446 Section 4.1.3 requires TLS 1.3 clients to check the last\n8 bytes of ServerHello.random for downgrade sentinel values when the\nserver negotiates TLS 1.2 or below. pure-tls does not perform this\ncheck in process-server-hello or any other function.\n\nWhile pure-tls only supports TLS 1.3 and rejects ServerHello without\nthe supported_versions extension indicating 1.3, a MITM attacker\ncould craft a response that passes the supported_versions check while\ndirecting the client toward a weaker configuration. If pure-tls is\never extended to support TLS 1.2 fallback, the absence of this check\nwould enable downgrade attacks.",
    "recommendation": "Add downgrade sentinel checking in the client handshake after\nreceiving ServerHello, even when only TLS 1.3 is negotiated. Check\nfor sentinel values 44 4F 57 4E 47 52 44 01 (TLS 1.2) and\n44 4F 57 4E 47 52 44 00 (TLS 1.1 or below)."
  },
  {
    "id": "CL-SEC-2026-0113",
    "title": "Missing ServerHello legacy_session_id_echo validation in pure-tls",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "medium",
    "cvss": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:L",
    "cvss-score": "4.8",
    "cwe": "CWE-345",
    "project-name": "pure-tls",
    "homepage": "https://github.com/atgreen/pure-tls",
    "introduced-commit": "a3076f0",
    "introduced-url": "https://github.com/atgreen/pure-tls/commit/a3076f0",
    "fixed-commit": "3d2db0d",
    "fixed-version": "1.11.0",
    "fixed-url": "https://github.com/atgreen/pure-tls/commit/3d2db0d",
    "description": "RFC 8446 Section 4.1.3 requires clients to abort the handshake with\nan illegal_parameter alert if the ServerHello legacy_session_id_echo\ndoes not match the legacy_session_id sent in the ClientHello. The\nprocess-server-hello function never performs this comparison, even\nthough the client-handshake struct stores the legacy-session-id.\n\nA MITM proxy could tamper with the session ID without detection,\npotentially confusing middleboxes or causing session-mismatch issues.",
    "recommendation": "After parsing ServerHello, compare server-hello-legacy-session-id-echo\nagainst client-handshake-legacy-session-id and abort with an\nillegal_parameter alert if they differ."
  },
  {
    "id": "CL-SEC-2026-0114",
    "title": "Non-cryptographic PRNG used for ticket age obfuscation in pure-tls",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "low",
    "cvss": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N",
    "cvss-score": "3.7",
    "cwe": "CWE-338",
    "project-name": "pure-tls",
    "homepage": "https://github.com/atgreen/pure-tls",
    "introduced-commit": "8d44750",
    "introduced-url": "https://github.com/atgreen/pure-tls/commit/8d44750",
    "fixed-commit": "3d2db0d",
    "fixed-version": "1.11.0",
    "fixed-url": "https://github.com/atgreen/pure-tls/commit/3d2db0d",
    "audit-verdict": "severity-adjusted",
    "audit-notes": "Low-impact cryptographic hygiene issue. Using non-CSPRNG for ticket_age_add is weak but has limited practical impact on confidentiality.",
    "description": "The server generates ticket_age_add values using Common Lisp's\n(random (expt 2 32)), which uses the implementation's default PRNG,\ntypically a Mersenne Twister or similar non-cryptographic generator.\nRFC 8446 Section 4.6.1 requires fresh values to obfuscate ticket age\nand prevent correlation attacks.\n\nAn observer who can see multiple NewSessionTicket messages may be\nable to predict future ticket_age_add values and de-obfuscate ticket\nages, enabling session correlation and traffic analysis. The\nrandom-grease-value function in src/constants.lisp also uses the\nnon-cryptographic (random ...) for GREASE values.",
    "recommendation": "Replace (random (expt 2 32)) with a cryptographically secure random\nsource, e.g., extracting 4 bytes from a CSPRNG and decoding them as\na uint32."
  },
  {
    "id": "CL-SEC-2026-0115",
    "title": "Secret key material not zeroized after use in pure-tls",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "low",
    "cvss": "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:N/A:N",
    "cvss-score": "2.5",
    "cwe": "CWE-226",
    "project-name": "pure-tls",
    "homepage": "https://github.com/atgreen/pure-tls",
    "introduced-commit": "0bf5413",
    "introduced-url": "https://github.com/atgreen/pure-tls/commit/0bf5413",
    "fixed-commit": "3d2db0d",
    "fixed-version": "1.11.0",
    "fixed-url": "https://github.com/atgreen/pure-tls/commit/3d2db0d",
    "audit-verdict": "severity-adjusted",
    "audit-notes": "Zeroization in GC'd Lisp is inherently best-effort. The library documents this limitation. Hardening gap, not medium-severity vulnerability. Go, Java, and Python face identical GC limitations.",
    "description": "Although pure-tls exports zeroize and with-zeroized-vector, these\nfunctions are never called anywhere in the codebase. Sensitive\ncryptographic material persists in memory indefinitely, including\nAEAD keys and IVs, traffic secrets in key-schedule structs, shared\nsecrets from DH key exchange, private keys loaded via\nload-private-key, and the global server ticket encryption key.\n\nIf an attacker gains a memory read primitive (via a separate\nvulnerability, cold boot attack, or core dump), they can recover\nTLS session keys and decrypt past and current sessions. This\nundermines TLS 1.3 forward secrecy guarantees which assume\nephemeral keys are destroyed.",
    "recommendation": "Call zeroize on all sensitive key material in unwind-protect cleanup\nforms. Use with-zeroized-vector for ephemeral secrets. While GC may\ncopy data, zeroizing reduces the exposure window as a defense-in-depth\nmeasure."
  },
  {
    "id": "CL-SEC-2026-0116",
    "title": "Gemini API key leaked in URL query parameter in cl-chat",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "high",
    "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
    "cvss-score": "7.5",
    "cwe": "CWE-598",
    "project-name": "cl-chat",
    "homepage": "https://github.com/atgreen/cl-chat",
    "introduced-commit": "649ee5d",
    "introduced-url": "https://github.com/atgreen/cl-completions/commit/649ee5d",
    "fixed-commit": "b63ba8e",
    "fixed-version": "1.1.0",
    "fixed-url": "https://github.com/atgreen/cl-completions/commit/b63ba8e",
    "description": "The Gemini completer in the cl-completions dependency embeds the API\nkey directly in the URL as a query parameter. This causes the key to\nappear in HTTP access logs, proxy logs, error messages (the\nsafe-http-request wrapper includes the request URI in errors), and\ndebug stream output. By contrast, the OpenAI and Anthropic completers\ncorrectly pass their keys via HTTP headers.",
    "recommendation": "Pass the Gemini API key in a request header instead of the URL.\nGoogle's Gemini API supports authentication via the x-goog-api-key\nheader as an alternative to the key= query parameter."
  },
  {
    "id": "CL-SEC-2026-0117",
    "title": "API keys leaked via error messages and debug logging in cl-chat",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "informational",
    "cvss": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:N",
    "cvss-score": "0.0",
    "cwe": "CWE-532",
    "project-name": "cl-chat",
    "homepage": "https://github.com/atgreen/cl-chat",
    "introduced-commit": "494e289",
    "introduced-url": "https://github.com/atgreen/cl-chat/commit/494e289",
    "fixed-commit": "d8b447c",
    "fixed-version": "1.1.0",
    "fixed-url": "https://github.com/atgreen/cl-chat/commit/d8b447c",
    "audit-verdict": "withdrawn",
    "audit-notes": "Advisory text describes code paths (safe-http-request, Gemini URI leakage) not found in cl-chat. The project's own fix notes describe a different issue (Hunchentoot error/backtrace exposure).",
    "description": "The safe-http-request error handler includes the full request URI in\nerror messages, which leaks the Gemini API key since it is embedded\nin the URL. API error response bodies are also propagated in error\nmessages and may echo back request details. When the debug stream is\nenabled, full API request payloads and responses are logged without\nredaction of sensitive fields.",
    "recommendation": "Redact API keys from error messages and debug output. Move the Gemini\nkey out of the URL (see CL-SEC-2026-0116). Implement a wrapper that\nstrips sensitive headers and parameters before logging."
  },
  {
    "id": "CL-SEC-2026-0118",
    "title": "Stored cross-site scripting via unsanitized WebSocket messages in cl-chat",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "high",
    "cvss": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:N",
    "cvss-score": "8.0",
    "cwe": "CWE-79",
    "project-name": "cl-chat",
    "homepage": "https://github.com/atgreen/cl-chat",
    "introduced-commit": "494e289",
    "introduced-url": "https://github.com/atgreen/cl-chat/commit/494e289",
    "fixed-commit": "d8b447c",
    "fixed-version": "1.1.0",
    "fixed-url": "https://github.com/atgreen/cl-chat/commit/d8b447c",
    "description": "User prompts are rendered through 3bmd (Markdown-to-HTML converter)\nand injected into the page via innerHTML and outerHTML on the client\nside. The Markdown-to-HTML conversion does not sanitize inline HTML,\nso a user prompt containing script tags or event handler attributes\nwill be converted to executable HTML.\n\nIn a multi-user scenario (shared session-data hash table and\nWebSocket chat rooms), this is a stored XSS vulnerability where one\nuser's malicious prompt is rendered in another user's browser.",
    "recommendation": "Sanitize HTML output from the Markdown renderer before DOM injection.\nUse textContent instead of innerHTML where possible, or use a DOM\nsanitization library like DOMPurify on the client side. Alternatively,\nconfigure 3bmd to strip raw HTML from Markdown input."
  },
  {
    "id": "CL-SEC-2026-0119",
    "title": "WebSocket and HTTP servers lack TLS encryption in cl-chat",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "low",
    "cvss": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:N",
    "cvss-score": "3.7",
    "cwe": "CWE-319",
    "project-name": "cl-chat",
    "homepage": "https://github.com/atgreen/cl-chat",
    "introduced-commit": "494e289",
    "introduced-url": "https://github.com/atgreen/cl-chat/commit/494e289",
    "fixed-commit": "d8b447c",
    "fixed-version": "1.1.0",
    "fixed-url": "https://github.com/atgreen/cl-chat/commit/d8b447c",
    "audit-verdict": "severity-adjusted",
    "audit-notes": "Deployment/defaults issue in a simple demo-style chat app. Not a hidden vulnerability in a reusable security library.",
    "description": "The WebSocket connection is hardcoded to use unencrypted ws://\n(not wss://), and the HTTP server is created without TLS. All chat\nmessages, including user prompts that may contain sensitive\ninformation, and LLM responses are transmitted in cleartext. In a\nnon-localhost deployment, this exposes all chat content and session\nidentifiers to eavesdropping and man-in-the-middle attacks.",
    "recommendation": "Configure TLS for both the HTTP and WebSocket servers. Use wss://\nfor WebSocket connections. At minimum, document that this is intended\nonly for localhost or development use."
  },
  {
    "id": "CL-SEC-2026-0120",
    "title": "Session cookie exposed in client-side JavaScript in cl-chat",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "medium",
    "cvss": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:N/A:N",
    "cvss-score": "6.8",
    "cwe": "CWE-614",
    "project-name": "cl-chat",
    "homepage": "https://github.com/atgreen/cl-chat",
    "introduced-commit": "494e289",
    "introduced-url": "https://github.com/atgreen/cl-chat/commit/494e289",
    "fixed-commit": "d8b447c",
    "fixed-version": "1.1.0",
    "fixed-url": "https://github.com/atgreen/cl-chat/commit/d8b447c",
    "description": "The Hunchentoot session cookie value is embedded directly in the HTML\npage as a JavaScript variable and sent over the WebSocket connection.\nCombined with the stored XSS vulnerability (CL-SEC-2026-0118), an\nattacker who injects JavaScript can extract the session ID and hijack\nanother user's session, gaining access to their chat history and LLM\ncompleter which may hold API keys. The session ID is also logged\nserver-side via log4cl.",
    "recommendation": "Do not embed the session cookie in JavaScript. Use HTTP-only cookies\nand authenticate WebSocket connections through the cookie mechanism.\nRemove session ID logging or redact it. Set HttpOnly and Secure flags\non session cookies."
  },
  {
    "id": "CL-SEC-2026-0121",
    "title": "Credentials sent in cleartext over non-TLS connections in cl-nats",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "medium",
    "cvss": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N",
    "cvss-score": "5.9",
    "cwe": "CWE-319",
    "project-name": "cl-nats",
    "homepage": "https://github.com/atgreen/cl-nats",
    "introduced-commit": "209806c",
    "introduced-url": "https://github.com/atgreen/cl-nats/commit/209806c",
    "fixed-commit": "074433b",
    "fixed-version": "1.0.0",
    "fixed-url": "https://github.com/atgreen/cl-nats/commit/074433b",
    "audit-verdict": "severity-adjusted",
    "audit-notes": "Sending credentials over non-TLS is an obvious consequence of choosing a non-TLS nats:// connection. Go nats.go and Python nats-py behave identically.",
    "description": "The build-connect-json function unconditionally includes user, pass,\nand auth_token fields in the CONNECT JSON payload. When the connection\nis not using TLS (nats:// URLs), these credentials are transmitted in\nplaintext over the wire. There is no check or warning when credentials\nare provided on a plain TCP connection. A network observer can\ntrivially intercept authentication tokens and passwords.",
    "recommendation": "At minimum, emit a warning when credentials are supplied on a non-TLS\nconnection. Ideally, require an explicit opt-in (e.g., :allow-\nplaintext-auth t) to send credentials over unencrypted connections."
  },
  {
    "id": "CL-SEC-2026-0122",
    "title": "No TLS certificate verification configuration in cl-nats",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "low",
    "cvss": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:N",
    "cvss-score": "3.7",
    "cwe": "CWE-295",
    "project-name": "cl-nats",
    "homepage": "https://github.com/atgreen/cl-nats",
    "introduced-commit": "209806c",
    "introduced-url": "https://github.com/atgreen/cl-nats/commit/209806c",
    "fixed-commit": "074433b",
    "fixed-version": "1.0.0",
    "fixed-url": "https://github.com/atgreen/cl-nats/commit/074433b",
    "audit-verdict": "disputed",
    "audit-notes": "pure-tls verifies certificates by default in the current environment. The MITM vulnerability framing is not established.",
    "description": "TLS connections are established via pure-tls:make-tls-client-stream\nwith only the :hostname parameter for SNI. There is no API to\nconfigure CA bundles, certificate pinning, whether to verify the\nserver certificate, or client certificates for mutual TLS. The\nbehavior depends entirely on pure-tls defaults which the user cannot\noverride.\n\nIf pure-tls does not verify certificates by default, this is a\nman-in-the-middle vulnerability. Even if it does verify, the user has\nno way to supply a custom CA bundle or disable verification for\ntesting.",
    "recommendation": "Expose TLS verification options: :tls-verify (boolean), :tls-ca-file\n(path to CA bundle), :tls-client-cert and :tls-client-key (for mTLS).\nPass these through to pure-tls:make-tls-client-stream."
  },
  {
    "id": "CL-SEC-2026-0123",
    "title": "NATS protocol injection via unsanitized subject and reply-to fields in cl-nats",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "medium",
    "cvss": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:N",
    "cvss-score": "6.8",
    "cwe": "CWE-93",
    "project-name": "cl-nats",
    "homepage": "https://github.com/atgreen/cl-nats",
    "introduced-commit": "209806c",
    "introduced-url": "https://github.com/atgreen/cl-nats/commit/209806c",
    "fixed-commit": "074433b",
    "fixed-version": "1.0.0",
    "fixed-url": "https://github.com/atgreen/cl-nats/commit/074433b",
    "audit-verdict": "severity-adjusted",
    "audit-notes": "Depends on application feeding attacker-controlled data into a low-level messaging API. Current code now validates, confirming prior risk.",
    "description": "Subject names, reply-to addresses, and queue group names are\ninterpolated directly into PUB, SUB, and HPUB wire protocol commands\nusing format with no validation. A subject containing CRLF characters\ncan inject arbitrary NATS protocol commands onto the wire, potentially\nsubscribing to unauthorized subjects or publishing rogue messages.\n\nThis is exploitable whenever user-controlled input flows into\npublish, subscribe, or request subject parameters.",
    "recommendation": "Validate all subject, reply-to, and queue-group strings before\ninterpolation. NATS subjects must not contain spaces, carriage\nreturns, newlines, or null bytes. Reject or signal an error on\ninvalid characters."
  },
  {
    "id": "CL-SEC-2026-0124",
    "title": "Unbounded memory allocation from malicious server MSG byte counts in cl-nats",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "high",
    "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
    "cvss-score": "7.5",
    "cwe": "CWE-770",
    "project-name": "cl-nats",
    "homepage": "https://github.com/atgreen/cl-nats",
    "introduced-commit": "209806c",
    "introduced-url": "https://github.com/atgreen/cl-nats/commit/209806c",
    "fixed-commit": "074433b",
    "fixed-version": "1.0.0",
    "fixed-url": "https://github.com/atgreen/cl-nats/commit/074433b",
    "description": "When the reader thread processes MSG or HMSG operations, it reads the\nbyte count from the parsed protocol line and immediately allocates a\nbuffer of that size via make-array. A malicious or compromised NATS\nserver can send a MSG line with an enormous byte count, causing the\nclient to attempt to allocate gigabytes of memory. There is no upper\nbound check against max_payload from the server INFO, and a malicious\nserver controls that value as well.",
    "recommendation": "Enforce a client-side maximum payload size (configurable, with a sane\ndefault like 64MB). Check the byte count before calling\ntransport-read-bytes. Validate that header-bytes is less than or\nequal to total-bytes in HMSG handling."
  },
  {
    "id": "CL-SEC-2026-0125",
    "title": "Credentials stored in plaintext in connection struct in cl-nats",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "low",
    "cvss": "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:N/A:N",
    "cvss-score": "2.5",
    "cwe": "CWE-256",
    "project-name": "cl-nats",
    "homepage": "https://github.com/atgreen/cl-nats",
    "introduced-commit": "209806c",
    "introduced-url": "https://github.com/atgreen/cl-nats/commit/209806c",
    "fixed-commit": "074433b",
    "fixed-version": "1.0.0",
    "fixed-url": "https://github.com/atgreen/cl-nats/commit/074433b",
    "audit-verdict": "severity-adjusted",
    "audit-notes": "Local memory/inspection exposure concern. Custom print-object now redacts credentials, addressing the most practical leak path.",
    "description": "The connection struct stores user, pass, and auth-token as plaintext\nstrings for the lifetime of the connection. These credentials persist\nin heap memory and may be exposed through core dumps, heap inspection\nby debuggers, the CL inspector in development environments, or the\nstruct's default print-object method which prints all slots.",
    "recommendation": "Override print-object for the connection struct to redact sensitive\nfields. Consider a :credential-provider callback pattern so\ncredentials are fetched on demand and not stored persistently. Zero\nout credential strings when the connection is closed."
  },
  {
    "id": "CL-SEC-2026-0126",
    "title": "Credential leakage in error messages and conditions in cl-nats",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "low",
    "cvss": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:N/A:N",
    "cvss-score": "3.1",
    "cwe": "CWE-209",
    "project-name": "cl-nats",
    "homepage": "https://github.com/atgreen/cl-nats",
    "introduced-commit": "209806c",
    "introduced-url": "https://github.com/atgreen/cl-nats/commit/209806c",
    "fixed-commit": "074433b",
    "fixed-version": "1.0.0",
    "fixed-url": "https://github.com/atgreen/cl-nats/commit/074433b",
    "audit-verdict": "severity-adjusted",
    "audit-notes": "Speculative exploit narrative. Current code reports host/port and generic write failures, not the CONNECT JSON containing credentials.",
    "description": "The connection failure path formats the last error into a user-visible\nerror message. If the error's printed representation includes the\nCONNECT JSON (which contains user, pass, and auth_token), credentials\nwill leak into log files, error handlers, or interactive debuggers.\nThe build-connect-json output is passed to format-connect and then to\ntransport-write-string, and any error during that write could capture\nthe CONNECT payload in its condition message.",
    "recommendation": "Ensure error messages never include the raw CONNECT JSON or credential\nvalues. Sanitize or redact error conditions before re-signaling them."
  },
  {
    "id": "CL-SEC-2026-0127",
    "title": "Server-supplied connect_urls enable SSRF with credential forwarding in cl-nats",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "low",
    "cvss": "CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:L/I:N/A:N",
    "cvss-score": "2.6",
    "cwe": "CWE-918",
    "project-name": "cl-nats",
    "homepage": "https://github.com/atgreen/cl-nats",
    "introduced-commit": "209806c",
    "introduced-url": "https://github.com/atgreen/cl-nats/commit/209806c",
    "fixed-commit": "074433b",
    "fixed-version": "1.0.0",
    "fixed-url": "https://github.com/atgreen/cl-nats/commit/074433b",
    "audit-verdict": "severity-adjusted",
    "audit-notes": "Server discovery is normal NATS clustering behavior. Trust-model and policy issue, not classic SSRF. *allow-discovered-servers* now exists for control.",
    "description": "During handshake and on reconnect, the client merges server-supplied\nconnect_urls from the INFO message into its server list without\nvalidation. A compromised or malicious NATS server can direct the\nclient to connect to arbitrary hosts and ports. The client will open\nTCP connections and send CONNECT JSON with credentials (user, pass,\nauth_token) to attacker-controlled endpoints.",
    "recommendation": "Provide a :no-discovered-servers option to disable merging. When\nmerging is enabled, restrict discovered URLs to the same subnet or\ndomain as configured servers. Do not send credentials to discovered\nservers that were not in the original configuration."
  },
  {
    "id": "CL-SEC-2026-0128",
    "title": "Header injection via CRLF in serialized NATS headers in cl-nats",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "low",
    "cvss": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:N",
    "cvss-score": "3.1",
    "cwe": "CWE-93",
    "project-name": "cl-nats",
    "homepage": "https://github.com/atgreen/cl-nats",
    "introduced-commit": "209806c",
    "introduced-url": "https://github.com/atgreen/cl-nats/commit/209806c",
    "fixed-commit": "074433b",
    "fixed-version": "1.0.0",
    "fixed-url": "https://github.com/atgreen/cl-nats/commit/074433b",
    "audit-verdict": "severity-adjusted",
    "audit-notes": "Exploitation depends on untrusted input passed directly into low-level protocol-building API. Current code now validates.",
    "description": "The serialize-headers function does not validate header names or\nvalues before formatting them into the wire protocol. A header name\nor value containing CRLF characters can inject additional header\nlines or terminate the header block early, potentially corrupting\nmessage framing.",
    "recommendation": "Validate that header names contain only printable ASCII without\ncolons, spaces, or control characters. Validate that header values\ndo not contain carriage return or newline characters. Reject invalid\nheaders with an error."
  },
  {
    "id": "CL-SEC-2026-0129",
    "title": "Double-encoded entity reversal enables XSS via output corruption in cl-sanitize-html",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "medium",
    "cvss": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:L/I:L/A:N",
    "cvss-score": "4.7",
    "cwe": "CWE-79",
    "project-name": "cl-sanitize-html",
    "homepage": "https://github.com/atgreen/cl-sanitize-html",
    "introduced-commit": "a93616f",
    "introduced-url": "https://github.com/atgreen/cl-sanitize-html/commit/a93616f",
    "fixed-commit": "10cc58c",
    "fixed-version": "1.0.0",
    "fixed-url": "https://github.com/atgreen/cl-sanitize-html/commit/10cc58c",
    "audit-verdict": "severity-adjusted",
    "audit-notes": "Post-serialization regex rewriting was a real structural problem (now fixed), but advisory overreaches with broad critical XSS claim without showing a concrete exploit.\n  CVSS re-scored from 8.0 (HIGH) to 4.7 (MEDIUM): C:H/I:H overstated for a theoretical, high-complexity, user-interaction-dependent bypass with no demonstrated exploit.",
    "description": "The function decode-double-encoded-entities runs as a post-processing\nstep on the final serialized HTML string, reversing Plump's safe\nentity encoding of &amp;gt;, &amp;lt;, &amp;quot;, and &amp;apos;\nback into their active forms. This post-serialization string rewriting\nis an anti-pattern that creates a fragile security boundary.\n\nUser-controlled text content containing literal entity-like strings\ncan be decoded into active HTML characters. The triple-encoding\nregex has a negative lookahead that can be bypassed with entity names\nnot in the exclusion list. Any change to Plump's serialization\nbehavior could make this directly exploitable for XSS.",
    "recommendation": "Remove decode-double-encoded-entities entirely. If Plump's\nserialization produces double-encoded entities, address this by\nconfiguring Plump's serializer or by custom serialization -- never\nby regex-replacing the output of a security-critical process."
  },
  {
    "id": "CL-SEC-2026-0130",
    "title": "Missing SVG/MathML foreign content handling enables XSS in cl-sanitize-html",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "critical",
    "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N",
    "cvss-score": "9.3",
    "cwe": "CWE-79",
    "project-name": "cl-sanitize-html",
    "homepage": "https://github.com/atgreen/cl-sanitize-html",
    "introduced-commit": "a93616f",
    "introduced-url": "https://github.com/atgreen/cl-sanitize-html/commit/a93616f",
    "fixed-commit": "10cc58c",
    "fixed-version": "1.0.0",
    "fixed-url": "https://github.com/atgreen/cl-sanitize-html/commit/10cc58c",
    "description": "The sanitizer has no special handling for SVG or MathML namespace\nelements. The dangerous-tag list that triggers full content removal\ndoes not include svg, math, object, iframe, embed, applet, base,\nmeta, link, or template. These tags are handled by the \"remove tag,\nkeep children\" path, which promotes their content into the document.\n\nSVG elements can contain foreignObject which re-enters HTML parsing\ncontext, and MathML's annotation-xml similarly re-enters HTML\nparsing. This enables mutation XSS (mXSS) where the sanitizer's DOM\ntree differs from the browser's re-parsing of the sanitized output.",
    "recommendation": "Add all potentially dangerous tags to the full-removal list: svg,\nmath, iframe, object, embed, applet, base, meta, link, template,\naudio, video, source, track, param, xmp, listing, and plaintext."
  },
  {
    "id": "CL-SEC-2026-0131",
    "title": "CSS comment bypass in style attribute sanitization in cl-sanitize-html",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "medium",
    "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N",
    "cvss-score": "6.1",
    "cwe": "CWE-79",
    "project-name": "cl-sanitize-html",
    "homepage": "https://github.com/atgreen/cl-sanitize-html",
    "introduced-commit": "a93616f",
    "introduced-url": "https://github.com/atgreen/cl-sanitize-html/commit/a93616f",
    "fixed-commit": "10cc58c",
    "fixed-version": "1.0.0",
    "fixed-url": "https://github.com/atgreen/cl-sanitize-html/commit/10cc58c",
    "audit-verdict": "severity-adjusted",
    "audit-notes": "Relies on legacy IE expression() behavior, sharply limiting modern practical impact. Real parsing gap but modern browsers are not affected.",
    "description": "The CSS sanitizer uses a naive semicolon-split parser and does not\nstrip CSS comments before performing danger checks. An attacker can\nuse CSS comments to break up dangerous keywords, e.g.,\n\"exp/**/ression(alert(1))\" bypasses the check for \"expression\"\nbecause the contiguous string does not appear in the value. After\nthe browser's CSS parser removes the comment, expression() executes\nJavaScript in legacy Internet Explorer.\n\nThe email policy allows the style attribute globally on all allowed\ntags, making this exploitable for any application using that policy.",
    "recommendation": "Strip CSS comments (/* ... */) from CSS values before performing\ndanger checks. Also strip them from the entire CSS string before\nparsing."
  },
  {
    "id": "CL-SEC-2026-0132",
    "title": "Protocol-relative URL bypass in cl-sanitize-html allows resource loading from arbitrary domains",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "medium",
    "cvss": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:N/A:N",
    "cvss-score": "4.1",
    "cwe": "CWE-20",
    "project-name": "cl-sanitize-html",
    "homepage": "https://github.com/atgreen/cl-sanitize-html",
    "introduced-commit": "a93616f",
    "introduced-url": "https://github.com/atgreen/cl-sanitize-html/commit/a93616f",
    "fixed-commit": "10cc58c",
    "fixed-version": "1.0.0",
    "fixed-url": "https://github.com/atgreen/cl-sanitize-html/commit/10cc58c",
    "description": "The protocol-allowed-p function checks if a URL starts with \"/\" and\nreturns true, intending to allow relative URLs. However, protocol-\nrelative URLs starting with \"//\" (e.g., \"//evil.com/track.gif\") also\npass this check, allowing an attacker to load resources from arbitrary\ndomains. This enables tracking pixels, data exfiltration via URL\nparameters, and phishing via image replacement.",
    "recommendation": "Check that relative URLs start with a single \"/\" not followed by\nanother \"/\". Explicitly reject protocol-relative URLs, or require\nan explicit scheme from the allowed protocols list."
  },
  {
    "id": "CL-SEC-2026-0133",
    "title": "Incomplete dangerous tag list allows content promotion from dangerous contexts in cl-sanitize-html",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "medium",
    "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N",
    "cvss-score": "6.1",
    "cwe": "CWE-79",
    "project-name": "cl-sanitize-html",
    "homepage": "https://github.com/atgreen/cl-sanitize-html",
    "introduced-commit": "a93616f",
    "introduced-url": "https://github.com/atgreen/cl-sanitize-html/commit/a93616f",
    "fixed-commit": "10cc58c",
    "fixed-version": "1.0.0",
    "fixed-url": "https://github.com/atgreen/cl-sanitize-html/commit/10cc58c",
    "audit-verdict": "severity-adjusted",
    "audit-notes": "Mostly a less precise duplicate of CL-SEC-2026-0130 rather than a distinct issue.",
    "description": "When a tag is not in the allowlist and not in the dangerous tags\nlist, it is removed but its children are promoted to the parent. The\ndangerous tags list is missing svg, math, iframe, object, embed,\ntemplate, xmp, listing, plaintext, base, meta, link, head, and\ntitle. These tags have their content promoted rather than fully\nremoved.\n\nThe most critical missing entry is svg, which enables mutation XSS\nthrough SVG/MathML namespace confusion where the sanitizer's DOM\ntree differs from the browser's re-parsing of the sanitized output.",
    "recommendation": "Add svg, math, iframe, object, embed, applet, template, xmp,\nlisting, plaintext, base, meta, link, head, and title to the\ndangerous tags list for full content removal."
  },
  {
    "id": "CL-SEC-2026-0134",
    "title": "Checksum validation optional and silently skipped in cl-selfupdate",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "medium",
    "cvss": "CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H",
    "cvss-score": "6.6",
    "cwe": "CWE-354",
    "project-name": "cl-selfupdate",
    "homepage": "https://github.com/atgreen/cl-selfupdate",
    "introduced-commit": "30134b7",
    "introduced-url": "https://github.com/atgreen/cl-selfupdate/commit/30134b7",
    "fixed-commit": "150b5d2",
    "fixed-version": "1.0.0",
    "fixed-url": "https://github.com/atgreen/cl-selfupdate/commit/150b5d2",
    "audit-verdict": "severity-adjusted",
    "audit-notes": "Bundles several complaints. Missing signature verification is a hardening gap, but not every checksum-only auto-updater should be treated as critical integrity failure.",
    "description": "The SHA256 validation system has multiple weaknesses. If no checksum\nasset (.sha256, checksums.txt, or SHA256SUMS) is present in the\nrelease, the download proceeds with zero integrity verification and\nno warning. Even when a checksum file is found, if the expected hash\ncannot be parsed for the specific asset, validation is silently\nskipped. The checksum file is fetched from the same unauthenticated\nchannel as the binary, so an attacker who can compromise the release\ncan replace both simultaneously. There is no GPG or minisign\nsignature verification.",
    "recommendation": "Implement cryptographic signature verification (GPG or minisign)\nwith a signing key pinned in the application, not fetched from the\nrelease. At minimum, warn loudly or error when no checksum is\navailable. Verify asset-size against the downloaded file size."
  },
  {
    "id": "CL-SEC-2026-0135",
    "title": "Predictable temporary directory enables symlink and race attacks in cl-selfupdate",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "high",
    "cvss": "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H",
    "cvss-score": "7.0",
    "cwe": "CWE-377",
    "project-name": "cl-selfupdate",
    "homepage": "https://github.com/atgreen/cl-selfupdate",
    "introduced-commit": "30134b7",
    "introduced-url": "https://github.com/atgreen/cl-selfupdate/commit/30134b7",
    "fixed-commit": "150b5d2",
    "fixed-version": "1.0.0",
    "fixed-url": "https://github.com/atgreen/cl-selfupdate/commit/150b5d2",
    "description": "The temporary directory used for downloads is constructed with a\npredictable name based on get-universal-time (wall-clock seconds).\nA local attacker can pre-create the directory or a symlink before the\nupdate runs. Since ensure-directories-exist does not fail if the\ndirectory already exists, the attacker controls where files are\nwritten. Between download and extraction, a local attacker can also\nswap the archive file (TOCTOU race). The temporary directory is\nnever cleaned up.",
    "recommendation": "Use mkdtemp(3) semantics for creating temporary directories with\nrandom, exclusive creation. Clean up temporary directories after\nuse in an unwind-protect. Open files with exclusive creation flags\nwhere possible."
  },
  {
    "id": "CL-SEC-2026-0136",
    "title": "No TLS certificate verification enforcement in cl-selfupdate",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "medium",
    "cvss": "CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H",
    "cvss-score": "6.6",
    "cwe": "CWE-295",
    "project-name": "cl-selfupdate",
    "homepage": "https://github.com/atgreen/cl-selfupdate",
    "introduced-commit": "30134b7",
    "introduced-url": "https://github.com/atgreen/cl-selfupdate/commit/30134b7",
    "fixed-commit": "150b5d2",
    "fixed-version": "1.0.0",
    "fixed-url": "https://github.com/atgreen/cl-selfupdate/commit/150b5d2",
    "audit-verdict": "severity-adjusted",
    "audit-notes": "Drakma backend uses :verify :required. Exposure depends on backend choice and TLS configuration. Not a blanket library-wide MITM.",
    "description": "Neither the dexador nor drakma HTTP backends explicitly configure TLS\ncertificate verification parameters. Whether certificates are verified\ndepends entirely on the underlying TLS library defaults. The library\nloads pure-tls/cl+ssl-compat by default, and the legacy cl+ssl\nfallback also does not verify certificates by default unless\nexplicitly configured. The library never sets :verify t or configures\nCA certificates.\n\nIf TLS verification is not active, an attacker in a network position\ncan intercept update downloads and serve a malicious binary.",
    "recommendation": "Explicitly configure TLS certificate verification in both HTTP\nbackends. For drakma, pass :verify :required and configure CA\ncertificates. For dexador, verify the underlying TLS library is\nconfigured to validate certificates. Document and test the TLS\nverification posture."
  },
  {
    "id": "CL-SEC-2026-0137",
    "title": "Archive path traversal in tar and zip extraction in cl-selfupdate",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "medium",
    "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N",
    "cvss-score": "6.5",
    "cwe": "CWE-22",
    "project-name": "cl-selfupdate",
    "homepage": "https://github.com/atgreen/cl-selfupdate",
    "introduced-commit": "30134b7",
    "introduced-url": "https://github.com/atgreen/cl-selfupdate/commit/30134b7",
    "fixed-commit": "150b5d2",
    "fixed-version": "1.0.0",
    "fixed-url": "https://github.com/atgreen/cl-selfupdate/commit/150b5d2",
    "description": "The tar extraction code uses file-namestring to extract the basename,\nbut its behavior with malicious input is implementation-defined. On\nsome CL implementations, crafted pathnames with path traversal\nsequences may not be fully sanitized. The zip extraction has the same\npattern.\n\nAdditionally, the executable-name matching uses substring search\nrather than exact match, meaning a crafted archive entry name like\n\"legit-app-evil-payload\" matches an executable-name of \"legit-app\".",
    "recommendation": "Validate that extracted paths are strictly within the output\ndirectory by resolving the full path and checking the prefix. Use\nexact basename matching rather than substring search for\nexecutable-name. Reject archive entries containing \"..\" path\ncomponents."
  },
  {
    "id": "CL-SEC-2026-0138",
    "title": "No protection against version downgrade attacks in cl-selfupdate",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "low",
    "cvss": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:N",
    "cvss-score": "3.7",
    "cwe": "CWE-757",
    "project-name": "cl-selfupdate",
    "homepage": "https://github.com/atgreen/cl-selfupdate",
    "introduced-commit": "30134b7",
    "introduced-url": "https://github.com/atgreen/cl-selfupdate/commit/30134b7",
    "fixed-commit": "150b5d2",
    "fixed-version": "1.0.0",
    "fixed-url": "https://github.com/atgreen/cl-selfupdate/commit/150b5d2",
    "audit-verdict": "severity-adjusted",
    "audit-notes": "No-downgrade protection now exists on high-level update-self path. Should be scoped to older versions or lower-level API, not described as present broad flaw.",
    "description": "The download-update function does not perform any version comparison.\nIf a caller passes a :release argument directly, there is no check\nthat the release version is newer than the current version. The\nfunction will install any release including older ones with known\nvulnerabilities.\n\nFurthermore, detect-latest fetches at most 30 releases per page. If\nan attacker can create enough fake releases to push the real latest\nbeyond page 1, detect-latest would return a stale version.",
    "recommendation": "Enforce version comparison in download-update and apply-update.\nPaginate through all releases in detect-latest, or increase the page\nsize and document the limitation."
  },
  {
    "id": "CL-SEC-2026-0139",
    "title": "No minimum RSA key size enforcement in cl-x509",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "high",
    "cvss": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:H",
    "cvss-score": "7.4",
    "cwe": "CWE-326",
    "project-name": "cl-x509",
    "homepage": "https://github.com/atgreen/cl-x509",
    "introduced-commit": "799828b",
    "introduced-url": "https://github.com/atgreen/cl-x509/commit/799828b",
    "fixed-commit": "2074636",
    "fixed-version": "1.1.0",
    "fixed-url": "https://github.com/atgreen/cl-x509/commit/2074636",
    "description": "The generate-rsa-key-pair function accepts an arbitrary :bits\nparameter with a default of 2048 but imposes no minimum. A caller\ncan generate keys of any size including fatally weak ones such as\n512, 256, or 64 bits. RSA-512 was factored publicly in 1999. NIST\nhas required a minimum of 2048 bits since 2013.\n\nIf cl-x509 is used to generate CA certificates with small keys, the\nentire PKI chain rooted in that CA is compromised. An attacker who\ncan factor the CA's modulus can forge arbitrary certificates.",
    "recommendation": "Enforce a minimum key size of 2048 bits in generate-rsa-key-pair.\nSignal a clear error for any request below this floor. Consider\ndefaulting to 4096 bits for CA certificates."
  },
  {
    "id": "CL-SEC-2026-0140",
    "title": "No validation of certificate validity period in cl-x509",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "medium",
    "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N",
    "cvss-score": "5.3",
    "cwe": "CWE-754",
    "project-name": "cl-x509",
    "homepage": "https://github.com/atgreen/cl-x509",
    "introduced-commit": "799828b",
    "introduced-url": "https://github.com/atgreen/cl-x509/commit/799828b",
    "fixed-commit": "2074636",
    "fixed-version": "1.1.0",
    "fixed-url": "https://github.com/atgreen/cl-x509/commit/2074636",
    "description": "The days parameter used to compute the certificate validity period\nis not validated. Negative values produce certificates where\nnot-after precedes not-before (malformed per RFC 5280 Section\n4.1.2.5). Zero produces a zero-second validity period. Extremely\nlarge values produce unreasonable validity periods. These malformed\ncertificates can cause validation failures, interoperability\nproblems, or create a false sense of security.",
    "recommendation": "Validate that days is a positive integer. Enforce a reasonable\nmaximum (e.g., 825 days for leaf certificates per CA/Browser Forum\nguidelines, or 36500 days as a sanity cap). Signal an error for\ninvalid inputs."
  },
  {
    "id": "CL-SEC-2026-0141",
    "title": "No CN/subject field validation allows null-byte injection in cl-x509",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "low",
    "cvss": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:N",
    "cvss-score": "3.7",
    "cwe": "CWE-20",
    "project-name": "cl-x509",
    "homepage": "https://github.com/atgreen/cl-x509",
    "introduced-commit": "799828b",
    "introduced-url": "https://github.com/atgreen/cl-x509/commit/799828b",
    "fixed-commit": "2074636",
    "fixed-version": "1.1.0",
    "fixed-url": "https://github.com/atgreen/cl-x509/commit/2074636",
    "audit-verdict": "severity-adjusted",
    "audit-notes": "Generation-side hardening against null-prefix ambiguity. Impact depends on whether downstream certificate consumers are vulnerable to the legacy attack.",
    "description": "The encode-name function passes CN, O, and OU values directly to\nencode-utf8-string without any validation. There is no check for\nstring length (DirectoryString maximum 64 characters for CN per\nRFC 5280), null bytes, empty strings, or control characters.\n\nA CN containing a null byte could enable null prefix attacks where\ncertificate validators see a truncated CN. For example,\nCN=\"evil.com\\0.legitimate.com\" might pass validation for\nlegitimate.com in vulnerable parsers (see CVE-2009-2408).",
    "recommendation": "Validate CN, O, and OU against RFC 5280 constraints: maximum 64\ncharacters for CN, reject null bytes and control characters, reject\nempty strings."
  },
  {
    "id": "CL-SEC-2026-0142",
    "title": "SAN DNS names not validated allowing wildcard and null-byte injection in cl-x509",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "low",
    "cvss": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:N",
    "cvss-score": "3.7",
    "cwe": "CWE-20",
    "project-name": "cl-x509",
    "homepage": "https://github.com/atgreen/cl-x509",
    "introduced-commit": "799828b",
    "introduced-url": "https://github.com/atgreen/cl-x509/commit/799828b",
    "fixed-commit": "2074636",
    "fixed-version": "1.1.0",
    "fixed-url": "https://github.com/atgreen/cl-x509/commit/2074636",
    "audit-verdict": "severity-adjusted",
    "audit-notes": "Certificate-authoring hardening. Practical impact depends on how vulnerable downstream consumers are to malformed SAN contents.",
    "description": "The SAN extension encoder accepts arbitrary DNS name strings without\nvalidation. There is no check for null bytes (enabling null-prefix\nattacks), malformed wildcards (*.*.example.com or foo*.example.com),\nempty labels, excessively long labels (DNS limit: 63 characters per\nlabel, 253 total), invalid characters, or non-ASCII characters\nwithout IDN/punycode conversion. IPv6 addresses in IP SANs are\nsilently unsupported with no error.",
    "recommendation": "Validate DNS names against RFC 952/1123 hostname rules and RFC 6125\nwildcard rules. Reject null bytes, validate label lengths, and\nrestrict wildcard placement. Add IPv6 support for IP SANs or signal\na clear error."
  },
  {
    "id": "CL-SEC-2026-0143",
    "title": "Private key material not zeroized in memory in cl-x509",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "low",
    "cvss": "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:N/A:N",
    "cvss-score": "2.5",
    "cwe": "CWE-316",
    "project-name": "cl-x509",
    "homepage": "https://github.com/atgreen/cl-x509",
    "introduced-commit": "799828b",
    "introduced-url": "https://github.com/atgreen/cl-x509/commit/799828b",
    "fixed-commit": "2074636",
    "fixed-version": "1.1.0",
    "fixed-url": "https://github.com/atgreen/cl-x509/commit/2074636",
    "audit-verdict": "severity-adjusted",
    "audit-notes": "GC behavior limits what zeroization can guarantee in any managed-memory language. Meaningful hygiene but overstated as exploitable vulnerability.",
    "description": "RSA private key components (d, p, q, dp, dq, qinv) are returned as\nCommon Lisp bignums and passed around as function parameters without\never being explicitly cleared from memory. Intermediate DER encodings\nand PEM string representations also persist in the Lisp heap. Common\nLisp's garbage collector does not guarantee freed memory is\noverwritten, and bignum values may be copied during GC compaction.\n\nCore dumps, swap files, or memory forensics can recover key material.",
    "recommendation": "For byte vectors containing key material (DER encodings, PEM\nstrings), provide explicit clearing using (fill array 0) after use.\nDocument the limitation clearly. Warn users that key material should\nbe used in short-lived processes."
  },
  {
    "id": "CL-SEC-2026-0144",
    "title": "No BasicConstraints enforcement for signing keys in cl-x509",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "informational",
    "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:N",
    "cvss-score": "0.0",
    "cwe": "CWE-295",
    "project-name": "cl-x509",
    "homepage": "https://github.com/atgreen/cl-x509",
    "introduced-commit": "799828b",
    "introduced-url": "https://github.com/atgreen/cl-x509/commit/799828b",
    "fixed-commit": "2074636",
    "fixed-version": "1.1.0",
    "fixed-url": "https://github.com/atgreen/cl-x509/commit/2074636",
    "audit-verdict": "withdrawn",
    "audit-notes": "Withdrawn per updated audit. cl-x509 is a certificate generation library, not a CA policy engine. Generating whatever certificate structure the caller requests is expected behavior \u2014 comparable to Go x509.CreateCertificate or Python cryptography's CertificateBuilder.",
    "description": "The generate-signed-certificate function accepts any RSA key pair\nas the signer with no verification that the signing key belongs to\na CA certificate with BasicConstraints CA=true. The extensions\nparameter is entirely optional, so a certificate can be generated\nwith no extensions at all. Some implementations treat missing\nBasicConstraints as CA=false, but others may not, creating\ncertificates that bypass path validation in lenient implementations.",
    "recommendation": "When extensions is nil in generate-signed-certificate, default to a\nsensible set (BasicConstraints CA=false, KeyUsage\ndigitalSignature+keyEncipherment). Consider adding an optional\n:ca-cert parameter that validates the signing certificate has\nCA=true."
  },
  {
    "id": "CL-SEC-2026-0145",
    "title": "KeyUsage unused-bits calculation incorrect for multi-byte bit strings in cl-x509",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "medium",
    "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N",
    "cvss-score": "5.3",
    "cwe": "CWE-682",
    "project-name": "cl-x509",
    "homepage": "https://github.com/atgreen/cl-x509",
    "introduced-commit": "799828b",
    "introduced-url": "https://github.com/atgreen/cl-x509/commit/799828b",
    "fixed-commit": "2074636",
    "fixed-version": "1.1.0",
    "fixed-url": "https://github.com/atgreen/cl-x509/commit/2074636",
    "description": "The Key Usage extension encoding uses a fragile unused-bits\ncalculation that only works for single-byte bit strings by\ncoincidence. The decipherOnly usage (bit 8 in RFC 5280) cannot be\nencoded correctly because the code only produces one byte. When no\nusages are specified, a meaningless extension is silently produced\nwith 7 unused bits and a zero byte.",
    "recommendation": "Rewrite the unused-bits calculation to handle multi-byte bit strings.\nReject empty usage sets. Add decipherOnly to the supported usages."
  },
  {
    "id": "CL-SEC-2026-0146",
    "title": "Shell command injection via unsanitized file paths in ocicl setup.lisp",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "informational",
    "cvss": null,
    "cvss-score": "0.0",
    "cwe": "CWE-78",
    "project-name": "ocicl",
    "homepage": "https://github.com/ocicl/ocicl",
    "introduced-commit": "8d7cf01",
    "introduced-url": "https://github.com/ocicl/ocicl/commit/8d7cf01",
    "audit-verdict": "withdrawn",
    "audit-notes": "setup.lisp is build-time tooling only, not part of the ocicl runtime.\n  It does not affect users of ocicl and is not a security concern.",
    "cve-id": null,
    "status": "withdrawn",
    "description": "The install-ocicl function in setup.lisp constructs shell command\nstrings using format directives with unsanitized pathname values,\nthen passes them to uiop:run-program as a single string (which\ninvokes a shell).\n\nOn line 98, the chmod command is built as:\n  (uiop:run-program (format nil \"chmod +x ~A\" path))\nOn lines 103-106, the ocicl setup invocation is built similarly:\n  (uiop:run-program (format nil \"~A setup\" path))\n\nThe destination directory is derived from the OCICL_PREFIX\nenvironment variable (line 18). If OCICL_PREFIX contains shell\nmetacharacters (semicolons, backticks, $(), pipes, etc.), those\nwill be interpreted by the shell, resulting in arbitrary command\nexecution during the build/install process.\n\nThis is exploitable when a user sets OCICL_PREFIX to a value\ncontrolled or influenced by an attacker, or when the path\ncontains spaces or special characters unintentionally.",
    "recommendation": "Pass arguments as a list to uiop:run-program instead of\nconstructing a shell command string. For example:\n\n  (uiop:run-program (list \"chmod\" \"+x\" (namestring path)))\n\nThis avoids shell interpretation entirely. Apply the same fix\nto the ocicl setup invocation on lines 103-106."
  },
  {
    "id": "CL-SEC-2026-0147",
    "title": "No client-side verification of downloaded package integrity in ocicl",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "medium",
    "cvss": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:N",
    "cvss-score": "6.8",
    "cwe": "CWE-345",
    "project-name": "ocicl",
    "homepage": "https://github.com/ocicl/ocicl",
    "introduced-commit": "170aff0",
    "introduced-url": "https://github.com/ocicl/ocicl/commit/170aff0",
    "fixed-commit": null,
    "fixed-version": null,
    "fixed-url": null,
    "cve-id": null,
    "status": "draft",
    "audit-verdict": "severity-adjusted",
    "audit-notes": "Client operates within OCI distribution model where content-addressed digests identify content. Advisory overstates if implying no integrity linkage at all.",
    "description": "When ocicl downloads a system package, it fetches an OCI manifest\ncontaining layer digests, then retrieves the blob by that digest\nURL. However, the client never independently verifies that the\nreceived blob content actually hashes to the claimed SHA-256\ndigest.\n\nThe project documents GPG signature verification and sigstore\nrekor transparency log integration in the README, but these are\nentirely manual processes. The download-system, get-blob, and\ndownload-and-install functions in src/ocicl.lisp perform no\ncryptographic verification of package content before extracting\nand installing it.\n\nThis means the client relies entirely on TLS transport security\nand registry-side digest enforcement. If TLS is disabled (via\n-k/--insecure flag or OCICL_INSECURE env var), or if the\nregistry itself is compromised, malicious packages can be\ninstalled without detection. Even with TLS enabled, a\ncompromised or malicious registry can serve arbitrary content.\n\nThe ocicl.csv file records the manifest digest alongside each\ninstalled system, but this digest is never verified against the\nactual content on disk after extraction.",
    "recommendation": "After downloading a blob, compute its SHA-256 hash using\nironclad (already a dependency) and compare it to the digest\nfrom the manifest before extracting:\n\n  (let ((computed (ironclad:byte-array-to-hex-string\n                   (ironclad:digest-sequence :sha256 blob-bytes))))\n    (unless (string= computed expected-digest)\n      (error \"Digest mismatch for ~A\" system)))\n\nAdditionally, consider making sigstore/GPG verification opt-out\nrather than opt-in, so that package authenticity is verified by\ndefault."
  },
  {
    "id": "CL-SEC-2026-0148",
    "title": "Untrusted search path via parent directory traversal for ocicl.csv",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "informational",
    "cvss": null,
    "cvss-score": "0.0",
    "cwe": "CWE-426",
    "project-name": "ocicl",
    "homepage": "https://github.com/ocicl/ocicl",
    "introduced-commit": "df82b60",
    "introduced-url": "https://github.com/ocicl/ocicl/commit/df82b60",
    "fixed-commit": null,
    "fixed-version": null,
    "fixed-url": null,
    "cve-id": null,
    "status": "withdrawn",
    "audit-verdict": "withdrawn",
    "audit-notes": "Upward config discovery is intentional behavior, not a vulnerability.\n  OCICL_LOCAL_ONLY exists to disable it when needed.",
    "description": "The find-workdir function in both src/ocicl.lisp (line 1603) and\nruntime/ocicl-runtime.lisp (line 211) traverses parent directories\nsearching for an ocicl.csv or systems.csv file. When found, that\nfile determines which systems directory is used and which packages\nare considered installed.\n\nAn attacker who can write files to a parent directory of the\nuser's working directory can place a malicious ocicl.csv that\nredirects package resolution. For example, in shared filesystem\nenvironments (/tmp, shared build directories, CI runners), an\nattacker could place an ocicl.csv in a common ancestor directory\nthat maps system names to attacker-controlled package content.\n\nWhen a user runs ocicl commands or loads packages via the\nocicl-runtime from any subdirectory beneath the attacker's\nplanted CSV, the malicious configuration takes precedence if no\nlocal ocicl.csv exists. This could cause the runtime to load\nattacker-supplied .asd and .lisp files, leading to arbitrary\ncode execution.\n\nThe OCICL_LOCAL_ONLY environment variable disables this\ntraversal behavior, but is not set by default.",
    "recommendation": "Consider one or more of the following mitigations:\n\n- Limit the upward traversal to a reasonable boundary (e.g.,\n  the user's home directory or a filesystem mount point).\n- Warn the user when a CSV file is found in a directory other\n  than the current working directory.\n- Skip directories that are world-writable or owned by a\n  different user.\n- Document the OCICL_LOCAL_ONLY option more prominently as a\n  security hardening measure."
  },
  {
    "id": "CL-SEC-2026-0160",
    "title": "Denial of service via read-from-string on URI port number",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "high",
    "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
    "cvss-score": "7.5",
    "cwe": "CWE-20",
    "project-name": "puri",
    "homepage": "https://github.com/llibra/puri",
    "introduced-commit": "0232acf",
    "introduced-url": "https://github.com/llibra/puri/commit/0232acf",
    "fixed-commit": "c0cb892",
    "fixed-url": "https://github.com/llibra/puri/commit/c0cb892",
    "cve-id": null,
    "status": "fixed",
    "description": "The URI parser uses Common Lisp's read-from-string to parse the port\ncomponent of a URI:\n\n    (setq port (read-from-string port))\n\nread-from-string invokes the full Lisp reader instead of a numeric\nparser, enabling denial of service through parse-uri:\n\n1. Floating-point overflow.  The URI state machine's port tokenizer\n   accepts digits, dots, and letters such as 'e', so a URI like:\n\n       http://example.com:1e100000000000000/\n\n   delivers the string \"1e100000000000000\" to read-from-string,\n   which signals a floating-point-overflow condition in most\n   implementations, crashing any handler without an enclosing\n   handler-case.\n\n2. Bignum allocation.  A port string containing millions of digit\n   characters (e.g. http://example.com:111...111/) causes\n   read-from-string to allocate an arbitrarily large bignum,\n   consuming CPU and memory.  The resulting integer passes both\n   the numberp and plusp checks, so no error is raised.\n\nAdditionally, no port range validation is performed.  Any positive\nnumber is accepted, including values far outside the valid TCP port\nrange of 1-65535.\n\nNote: while read-from-string can also execute arbitrary code via the\n#. reader macro when *read-eval* is T (the default), the URI parser's\nstate machine treats # as a fragment delimiter, preventing #. from\nreaching the port code path through parse-uri.  The #. vector is only\nreachable if application code passes unsanitized strings to\nread-from-string outside of parse-uri.",
    "recommendation": "Replace read-from-string with parse-integer and add port range\nvalidation:\n\n    (setq port (parse-integer port :junk-allowed nil))\n    (when (not (<= 1 port 65535))\n      (error \"port not in valid range: ~d.\" port))\n\nparse-integer accepts only decimal integers, rejecting floats and\nreader macros.  The range check prevents bignum allocation abuse\nand ensures only valid TCP port numbers are accepted."
  },
  {
    "id": "CL-SEC-2026-0179",
    "title": "trivia read pattern calls READ-FROM-STRING without binding *READ-EVAL* to NIL",
    "reported": "2026-03-31",
    "published": "2026-03-31",
    "severity": "informational",
    "cvss": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:N",
    "cvss-score": "0.0",
    "cwe": "CWE-94",
    "project-name": "trivia",
    "homepage": "https://github.com/guicho271828/trivia",
    "introduced-commit": "3277444",
    "introduced-url": "https://github.com/guicho271828/trivia/commit/3277444",
    "cve-id": null,
    "status": "withdrawn",
    "audit-verdict": "withdrawn",
    "audit-notes": "Withdrawn. The `read` pattern is a compile-time macro that expands into code the\n  developer explicitly wrote. Using it on untrusted input is a developer choice, not\n  a hidden trap \u2014 equivalent to calling read-from-string directly. A PR (#154) proposing\n  a *read-eval* nil binding as defense-in-depth was submitted but closed without merging,\n  as the original behavior is working-as-designed, not a vulnerability in trivia itself.",
    "description": "The `read` pattern in trivia (level2/derived2.lisp, lines 32-44)\nexpands to a runtime call to READ-FROM-STRING on the value being\nmatched, without binding *READ-EVAL* to NIL.  Common Lisp's reader\nevaluates #.(form) reader macros at read time when *READ-EVAL* is T\n(the default), which means an attacker who controls the string being\nmatched can achieve arbitrary code execution.\n\nFor example:\n\n    (match user-supplied-string\n      ((read x) (format t \"Parsed: ~a\" x)))\n\nIf user-supplied-string is \"#.(delete-file \\\"/etc/important\\\")\", the\nreader will execute the deletion before the match even completes.\n\nThe pattern also silently swallows END-OF-FILE and PARSE-ERROR\nconditions, masking failed exploitation attempts and making the\nvulnerability harder to detect in logs.",
    "recommendation": "Bind *READ-EVAL* to NIL around the READ-FROM-STRING call:\n\n    `(guard1 (,it :type string) (stringp ,it)\n             (let ((*read-eval* nil))\n               (handler-case (read-from-string ,it)\n                 (end-of-file ())\n                 (parse-error ()))) ,pattern)\n\nAdditionally, consider documenting that the `read` pattern should\nnever be used on untrusted input even with this fix, as the CL\nreader has a large attack surface beyond just *READ-EVAL*."
  },
  {
    "id": "CL-SEC-2026-0197",
    "title": "Unbounded memory allocation in protobuf length-delimited field decoding",
    "reported": "2026-04-03",
    "published": "2026-04-03",
    "severity": "high",
    "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
    "cvss-score": "7.5",
    "cwe": "CWE-770",
    "project-name": "ag-proto",
    "homepage": "https://github.com/atgreen/ag-gRPC",
    "introduced-commit": "f76354d",
    "introduced-url": "https://github.com/atgreen/ag-gRPC/commit/f76354d",
    "fixed-commit": "aa8cb9c",
    "fixed-version": "1.4.0",
    "fixed-url": "https://github.com/atgreen/ag-gRPC/commit/aa8cb9c",
    "cve-id": null,
    "status": "fixed",
    "description": "The ag-proto library's decode-length-delimited-from-stream function\n(wire-format.lisp:293-299) allocates a byte vector based on a length\nvalue decoded from the input stream:\n\n    (let* ((length (decode-varint-from-stream stream))\n           (data (make-array length :element-type '(unsigned-byte 8))))\n      (read-sequence data stream)\n      data)\n\nThe varint-decoded length can be up to 2^63.  A malicious protobuf\nmessage can specify an enormous length for a string, bytes, or\nembedded message field, causing the decoder to attempt allocating\ngigabytes of memory.\n\nThis affects all deserialization paths including string decoding\n(decode-string-from-stream, line 319-321) and any generated message\ndeserializer that processes length-delimited fields.\n\nAdditionally, decode-field-tag-from-stream (line 361-376) lacks the\nshift > 63 check present in decode-varint-from-stream, allowing a\nmalicious input with continuous high-bit-set bytes to cause unbounded\nloop iterations.",
    "recommendation": "1. Add a configurable maximum message size (e.g., 64 MB default)\n   and check the decoded length against it before allocation.\n2. Add the shift > 63 overflow check to decode-field-tag-from-stream.\n3. Consider also validating that the decoded length does not exceed\n   the remaining bytes in the stream when the stream length is known."
  },
  {
    "id": "CL-SEC-2026-0201",
    "title": "Certificate chain trust anchor verified by issuer name only, not signature, in pure-tls",
    "reported": "2026-04-18",
    "published": "2026-04-18",
    "severity": "high",
    "cvss": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N",
    "cvss-score": "7.4",
    "cwe": "CWE-295",
    "project-name": "pure-tls",
    "homepage": "https://github.com/atgreen/pure-tls",
    "introduced-commit": "a3076f0",
    "introduced-url": "https://github.com/atgreen/pure-tls/commit/a3076f0",
    "fixed-commit": "0fefa53",
    "fixed-version": "1.11.1",
    "fixed-url": "https://github.com/atgreen/pure-tls/commit/0fefa53",
    "description": "The verify-certificate-chain function accepted a certificate chain as\ntrusted if any chain member's issuer distinguished name matched a\ntrusted root certificate, without verifying the cryptographic\nsignature against that root's public key.  The check used\ncertificate-issued-by-p (issuer name equality) as a sufficient\ncondition for trust anchoring.\n\nAn attacker could construct a certificate chain containing a forged\nintermediate whose issuer DN matches a well-known root CA.  Since\nthe signature was never checked, this forged intermediate would pass\nthe trust anchor test, and any end-entity certificate chaining to it\nwould be accepted.  This enables man-in-the-middle interception\nwithout requiring CA compromise \u2014 only knowledge of the target root\nCA's distinguished name.\n\nThe fix requires that chain-to-anchor verification calls\nverify-certificate-signature in addition to certificate-issued-by-p,\nensuring the chain is cryptographically bound to the trust anchor.",
    "recommendation": "Update to pure-tls 1.11.1 or later.  If updating is not possible,\nset the OCICL_TLS_DEBUG environment variable to monitor chain\nverification failures."
  },
  {
    "id": "CL-SEC-2026-0202",
    "title": "Constant-time comparison returns equal for inputs differing by a multiple of 256 bytes in length",
    "reported": "2026-04-18",
    "published": "2026-04-18",
    "severity": "high",
    "cvss": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N",
    "cvss-score": "5.9",
    "cwe": "CWE-697",
    "project-name": "pure-tls",
    "homepage": "https://github.com/atgreen/pure-tls",
    "introduced-commit": "03e03cc",
    "introduced-url": "https://github.com/atgreen/pure-tls/commit/03e03cc",
    "fixed-commit": "7c0a399",
    "fixed-version": "1.11.1",
    "fixed-url": "https://github.com/atgreen/pure-tls/commit/7c0a399",
    "description": "The ct-equal-mask function in src/utils.lisp masked the XOR of the\ntwo input lengths with #xFF before accumulating into the diff\nvariable.  When two inputs differ in length by a multiple of 256,\nthe masked length XOR is zero, and if the first min(len-a, len-b)\nbytes are identical, ct-equal-mask returns #xFF (equal).\n\nThis function is used in ML-KEM-768 decapsulation (src/crypto/ml-kem.lisp)\nto compare the re-encrypted ciphertext against the received ciphertext\nfor implicit rejection (FIPS 203 Section 7.3).  A false \"equal\"\nresult causes decapsulation to return the real shared secret instead\nof the rejection key, breaking IND-CCA2 security.\n\nIn practice, ML-KEM ciphertexts are fixed-length within a parameter\nset, which limits immediate exploitability.  However, the function\nis a general-purpose primitive and any future use with variable-length\ninputs (MACs, hashes, authentication tokens) would be directly\nexploitable as an authentication bypass.\n\nThe fix accumulates the full fixnum XOR of lengths and folds all\nbytes together before the mask derivation, ensuring any non-zero\nlength difference produces a \"not equal\" result.",
    "recommendation": "Update to pure-tls 1.11.1 or later.  Review any code that calls\nct-equal-mask with potentially variable-length inputs."
  },
  {
    "id": "CL-SEC-2026-0203",
    "title": "Unbounded recursion in protobuf nested message deserialization",
    "reported": "2026-04-20",
    "published": "2026-04-20",
    "severity": "high",
    "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
    "cvss-score": "7.5",
    "cwe": "CWE-674",
    "project-name": "ag-proto",
    "homepage": "https://github.com/atgreen/ag-gRPC",
    "introduced-commit": "f76354d",
    "introduced-url": "https://github.com/atgreen/ag-gRPC/commit/f76354d",
    "fixed-commit": "d158251",
    "fixed-version": "1.5.0",
    "fixed-url": "https://github.com/atgreen/ag-gRPC/commit/d158251",
    "cve-id": null,
    "status": "fixed",
    "description": "Generated deserialize-from-bytes methods for protobuf messages recurse\ninto nested message fields without any depth limit.  A malicious\nprotobuf payload containing deeply nested embedded messages (e.g.,\nmessage A containing message A containing message A...) causes\nunbounded stack recursion, crashing the process with a stack overflow.\n\nThe relevant code path in codegen.lisp generates:\n\n    (let ((data (read-length-delimited buffer)))\n      (deserialize-from-bytes 'NESTED-TYPE data))\n\nEach nested message triggers another call to deserialize-from-bytes\nwith no depth tracking.  An attacker who can send arbitrary protobuf\nmessages to a gRPC server (or trick a client into deserializing a\ncrafted response) can cause denial of service.\n\nThis affects all generated message types that contain embedded message\nfields, including recursive/self-referential message definitions.",
    "recommendation": "Upgrade to ag-gRPC >= 1.5.0, which adds *max-recursion-depth*\n(default 100, matching the protobuf spec recommendation).  The limit\nis enforced before each recursive deserialize-from-bytes call.\n\nTo adjust the limit:\n\n    (setf ag-proto:*max-recursion-depth* 50)   ; stricter\n    (setf ag-proto:*max-recursion-depth* nil)   ; disable (not recommended)"
  },
  {
    "id": "CL-SEC-2026-0204",
    "title": "No decompressed size limit in gRPC message decompression (gzip bomb)",
    "reported": "2026-04-20",
    "published": "2026-04-20",
    "severity": "high",
    "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
    "cvss-score": "7.5",
    "cwe": "CWE-409",
    "project-name": "ag-grpc",
    "homepage": "https://github.com/atgreen/ag-gRPC",
    "introduced-commit": "f76354d",
    "introduced-url": "https://github.com/atgreen/ag-gRPC/commit/f76354d",
    "fixed-commit": "d158251",
    "fixed-version": "1.5.0",
    "fixed-url": "https://github.com/atgreen/ag-gRPC/commit/d158251",
    "cve-id": null,
    "status": "fixed",
    "description": "The gRPC message decompression functions gzip-decompress and\ndecompress-grpc-message (framing.lisp) decompress the full payload\ninto memory without checking the resulting size.  A small compressed\ngRPC message (e.g., a few kilobytes of gzip-compressed zeros) can\nexpand to gigabytes, exhausting available memory and crashing the\nprocess.\n\nThe gzip decompression path was:\n\n    (chipz:decompress nil 'chipz:gzip simple-data)\n\nThe result is returned without any size validation.  The existing\n*max-message-size* limit (CL-SEC-2026-0197) only checks the\ncompressed wire size, not the decompressed output.\n\nThis affects both server-side request decompression and client-side\nresponse decompression when gzip or deflate encoding is negotiated\nvia the grpc-encoding header.",
    "recommendation": "Upgrade to ag-grpc >= 1.5.0, which adds *max-decompressed-size*\n(default 16 MiB).  The decompressed output size is checked before\nthe data is returned to the caller.\n\nTo adjust the limit:\n\n    (setf ag-grpc:*max-decompressed-size* (* 64 1024 1024))  ; 64 MiB\n    (setf ag-grpc:*max-decompressed-size* nil)                ; disable (not recommended)"
  },
  {
    "id": "CL-SEC-2026-0206",
    "title": "Out-of-bounds read parsing attacker-supplied ECHConfig causes uncaught error (remote DoS) in pure-tls",
    "reported": "2026-06-21",
    "published": "2026-06-21",
    "severity": "low",
    "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L",
    "cvss-score": "5.3",
    "cwe": "CWE-125",
    "project-name": "pure-tls",
    "homepage": "https://github.com/atgreen/pure-tls",
    "introduced-commit": "c29b714",
    "introduced-url": "https://github.com/atgreen/pure-tls/commit/c29b714",
    "fixed-commit": "9f9f974",
    "fixed-version": "1.11.2",
    "fixed-url": "https://github.com/atgreen/pure-tls/commit/9f9f974",
    "audit-verdict": "severity-adjusted",
    "audit-notes": "CVSS base vector computes to 5.3 (medium), but real-world impact is\n  limited to a single client-to-malicious-server connection: the\n  handshake aborts with an uncaught Lisp condition, with no effect on\n  other connections, no memory disclosure (SBCL bounds-checks AREF/SUBSEQ\n  and raises a condition rather than reading wild memory), and no data or\n  integrity impact. Adjusted to low. The primary attack surface is a\n  pure-tls *client* processing a hostile server's EncryptedExtensions; the\n  server-side ClientHello ECH path uses the bounds-checked tls-buffer\n  readers and is not affected.",
    "description": "The Encrypted Client Hello (ECH) configuration parser in\nsrc/handshake/ech.lisp reads attacker-controlled 16-bit and 8-bit\nlength fields (public_key, cipher_suites, public_name, extensions) and\nslices the input with raw AREF/SUBSEQ *before* performing its only\nbounds check (the `(<= pos end)` test at the end of\nparse-ech-config-contents).  An oversized length field therefore makes\nSUBSEQ raise SB-KERNEL:BOUNDING-INDICES-BAD-ERROR \u2014 an ordinary CL\nerror that is NOT a subtype of pure-tls:tls-error.\n\nA pure-tls client reaches this path when it parses the ECH extension in\nan EncryptedExtensions message (parse-ech-extension dispatches to\nparse-ech-config-list for the :encrypted-extensions context).  This\nhappens during normal message parsing even if the client never offered\nECH.  The handshake error handlers only catch tls-* conditions, so the\nraw bounds error propagates uncaught and terminates the handshake (and,\ndepending on how the caller drives the connection, the worker thread).\n\nProof of concept (an 11-byte ECHConfigList whose declared public_key\nlength is 0xFFFF):\n\n    00 09 fe 0d 00 05 00 00 20 ff ff\n\nparse-ech-config-list accepts the outer length, parse-ech-config reads\nversion 0xfe0d and contents length 5, and parse-ech-config-contents\nthen evaluates (subseq data 11 65546) over an 11-byte buffer, raising\n\"The bounding indices 11 and 65546 are bad for a sequence of length 11.\"",
    "recommendation": "Update to pure-tls 1.11.2 or later.  The fix parses ECHConfig\nstructures through the bounds-checked tls-buffer readers\n(buffer-read-octet / buffer-read-uint16 / buffer-read-vector8 /\nbuffer-read-vector16), which validate every length against the\nremaining buffer and signal a graceful tls-decode-error instead of a\nraw Lisp condition."
  },
  {
    "id": "CL-SEC-2026-0207",
    "title": "ExtendedKeyUsage not enforced during certificate chain verification in pure-tls",
    "reported": "2026-06-21",
    "published": "2026-06-21",
    "severity": "low",
    "cvss": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N",
    "cvss-score": "4.2",
    "cwe": "CWE-295",
    "project-name": "pure-tls",
    "homepage": "https://github.com/atgreen/pure-tls",
    "introduced-commit": "dfd866b",
    "introduced-url": "https://github.com/atgreen/pure-tls/commit/dfd866b",
    "fixed-commit": "9f9f974",
    "fixed-version": "1.11.2",
    "fixed-url": "https://github.com/atgreen/pure-tls/commit/9f9f974",
    "audit-verdict": "severity-adjusted",
    "audit-notes": "EKU non-enforcement is primarily a defense-in-depth / RFC 5280 +\n  CA/Browser Forum compliance gap.  Direct impersonation requires the\n  attacker to already possess a validly-chained certificate that differs\n  from a usable one only by its ExtendedKeyUsage \u2014 most realistically an\n  EKU-technically-constrained subordinate CA in the trust path, or a\n  certificate mis-issued for the wrong purpose.  Because that precondition\n  must already hold, the practical severity is low even though the\n  consequence in that scenario (acceptance of a certificate for an\n  unintended purpose, enabling MITM) is serious.",
    "description": "The pure-Lisp certificate chain verifier (verify-certificate-chain in\nsrc/x509/verify.lisp) validated dates, issuer names, BasicConstraints\ncA, keyCertSign, path-length, and signatures, but never inspected the\nExtendedKeyUsage (EKU) extension.  EKU was even listed among the\n\"known\" critical extensions (src/x509/certificate.lisp), so a leaf\ncarrying a *critical* EKU that omits id-kp-serverAuth was accepted\nsilently as a TLS server certificate; symmetrically, a client\ncertificate need not carry id-kp-clientAuth.\n\nConcretely, a leaf with EKU = clientAuth only (no serverAuth) is\naccepted when validating a server's chain:\n\n    (verify-certificate-chain (list clientauth-only-leaf) (list root))\n    => T          ; should be rejected for TLS server authentication\n\nThe risk is that EKU is a relied-upon technical constraint: PKIs\ndelegate subordinate CAs that are constrained (by EKU and/or\nnameConstraints) so that they cannot mint certificates usable for TLS\nserver authentication.  By ignoring EKU, pure-tls removes that\ncontainment, so a certificate (or sub-CA) constrained away from\nserverAuth could nonetheless be used to authenticate a TLS server,\nenabling man-in-the-middle interception.\n\nNote: this is the surviving half of an earlier internal finding whose\nnameConstraints component proved to be a false positive \u2014 pure-tls\nfails *closed* on a critical nameConstraints extension because the\nnameConstraints OID (2.5.29.30) is not recognized, so such certificates\nare rejected at parse time.",
    "recommendation": "Update to pure-tls 1.11.2 or later.  The fix adds a `purpose` parameter\nto verify-certificate-chain (the TLS client path requests :server-auth,\nthe server path requests :client-auth) and rejects a leaf whose EKU\nextension is present but contains neither the requested purpose nor\nanyExtendedKeyUsage.  Per RFC 5280, a certificate with no EKU extension\nis treated as unrestricted and continues to be accepted."
  },
  {
    "id": "CL-SEC-2026-0208",
    "title": "Unbounded request buffering in HTTP/2 server (no max-receive-message-size)",
    "reported": "2026-06-21",
    "published": "2026-06-21",
    "severity": "high",
    "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
    "cvss-score": "7.5",
    "cwe": "CWE-770",
    "project-name": "ag-gRPC",
    "homepage": "https://github.com/atgreen/ag-gRPC",
    "introduced-commit": "f76354d",
    "introduced-url": "https://github.com/atgreen/ag-gRPC/commit/f76354d",
    "fixed-commit": "13a0b93",
    "fixed-version": "1.5.7",
    "fixed-url": "https://github.com/atgreen/ag-gRPC/commit/13a0b93",
    "cve-id": null,
    "status": "fixed",
    "description": "The HTTP/2 receive path buffers inbound DATA frames per stream with no\nupper bound, and the receiver's flow-control window provides no\nbackpressure, so a single unauthenticated peer can drive the server to\nexhaust memory.\n\nIn connection.lisp, process-frame appends every DATA payload to the\nstream's buffer:\n\n    (stream-append-data stream (frame-payload frame))\n\nstream-append-data (streams.lisp) is an unbounded vector-push-extend\nloop.  Immediately afterwards the handler decrements the local\nflow-control window and then restores it while emitting a WINDOW_UPDATE\nfor the full payload:\n\n    (decf (window-size (connection-local-window conn)) data-length)\n    ... write WINDOW_UPDATE(0, data-length) / WINDOW_UPDATE(stream, data-length) ...\n    (incf (window-size (connection-local-window conn)) data-length)\n\nThe net window change is zero and the peer is always told it may send\nmore, so flow control never throttles a sender.  There is no\nmax-receive-message-size anywhere in ag-http2 or ag-grpc.\n\nExploit: after the HTTP/2 handshake, an unauthenticated peer opens a\nstream and streams DATA frames without ever setting END_STREAM (or\ndeclares a large gRPC length prefix and trickles the body).  Because the\nbytes are buffered at the HTTP/2 layer before any handler/gRPC\ndispatch, the per-stream buffer grows without limit until the process is\nkilled by the OOM killer, taking down the whole server for all clients.\nThe streaming-RPC message queue (stream-message-buffer in server.lisp)\nhas the same unbounded shape when the application handler is slower than\nthe sender.\n\nThis is the same class of remote, unauthenticated memory-exhaustion DoS\nas CL-SEC-2026-0204 (decompression bomb) and CL-SEC-2026-0197\n(length-delimited allocation), and is made cheaper by CL-SEC-2026-0209\n(no SETTINGS_MAX_FRAME_SIZE enforcement, allowing 16 MB DATA frames).",
    "recommendation": "Bound the buffered-but-unconsumed request data per stream and fail the\nconnection once the bound is exceeded.  The working-tree fix adds:\n\n    ag-http2:*max-receive-buffer-size*  ; default 4 MiB (gRPC default\n                                        ; max receive message size); NIL disables\n\nand, in the DATA-frame handler, rejects a stream whose buffered data\nexceeds the cap with an ENHANCE_YOUR_CALM connection error before doing\nfurther work.  This bounds memory while preserving throughput for\nlegitimate transfers (the window is still replenished within the cap).\n\nOperators can tune the limit:\n\n    (setf ag-http2:*max-receive-buffer-size* (* 16 1024 1024))  ; 16 MiB\n    (setf ag-http2:*max-receive-buffer-size* nil)               ; disable (not recommended)"
  },
  {
    "id": "CL-SEC-2026-0209",
    "title": "HTTP/2 SETTINGS_MAX_FRAME_SIZE not enforced on inbound frames",
    "reported": "2026-06-21",
    "published": "2026-06-21",
    "severity": "high",
    "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
    "cvss-score": "7.5",
    "cwe": "CWE-770",
    "project-name": "ag-gRPC",
    "homepage": "https://github.com/atgreen/ag-gRPC",
    "introduced-commit": "f76354d",
    "introduced-url": "https://github.com/atgreen/ag-gRPC/commit/f76354d",
    "fixed-commit": "13a0b93",
    "fixed-version": "1.5.7",
    "fixed-url": "https://github.com/atgreen/ag-gRPC/commit/13a0b93",
    "cve-id": null,
    "status": "fixed",
    "description": "read-frame (frames.lisp) parses the 24-bit frame length from the wire\nand unconditionally allocates a payload buffer of that size before\nreading the body:\n\n    (length (logior (ash (aref header 0) 16)\n                    (ash (aref header 1) 8)\n                    (aref header 2)))\n    ...\n    (payload (make-array length :element-type '(unsigned-byte 8)))\n\nThe declared length may be up to 2^24-1 (16 MiB).  RFC 7540 section 4.2\nrequires that a frame exceeding the receiver's advertised\nSETTINGS_MAX_FRAME_SIZE (default and minimum 16384) be treated as a\nconnection error of type FRAME_SIZE_ERROR.  The server advertises\nmax-frame-size 16384 in its SETTINGS but never checks inbound frames\nagainst it.\n\nExploit: an unauthenticated peer sends a 9-byte frame header declaring a\nlength of up to 16 MiB and then stalls.  read-frame allocates the full\nbuffer and blocks in read-full-sequence waiting for bytes that never\narrive.  With the default max-connections of 128, roughly 2 GiB of\nserver memory can be pinned with only a few bytes of traffic per\nconnection (a ~1.8-million-to-one amplification from the 9-byte header).\nBecause the bound is per frame, repeated oversize frames also feed the\nunbounded buffering issue described in CL-SEC-2026-0208.",
    "recommendation": "Reject frames larger than the advertised SETTINGS_MAX_FRAME_SIZE before\nallocating the payload.  The working-tree fix:\n\n  * adds the constant ag-http2::+default-max-frame-size+ (16384);\n  * gives read-frame an optional max-frame-size argument and signals\n    http2-frame-error with error-code FRAME_SIZE_ERROR when\n    (> length max-frame-size), before make-array; and\n  * has connection-read-frame pass the connection's locally-advertised\n    SETTINGS_MAX_FRAME_SIZE."
  },
  {
    "id": "CL-SEC-2026-0210",
    "title": "gRPC client crash on malformed percent-encoded grpc-message trailer",
    "reported": "2026-06-21",
    "published": "2026-06-21",
    "severity": "low",
    "cvss": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L",
    "cvss-score": "3.7",
    "cwe": "CWE-248",
    "project-name": "ag-gRPC",
    "homepage": "https://github.com/atgreen/ag-gRPC",
    "introduced-commit": "f76354d",
    "introduced-url": "https://github.com/atgreen/ag-gRPC/commit/f76354d",
    "fixed-commit": "13a0b93",
    "fixed-version": "1.5.7",
    "fixed-url": "https://github.com/atgreen/ag-gRPC/commit/13a0b93",
    "cve-id": null,
    "status": "fixed",
    "description": "The gRPC client decodes the peer-supplied grpc-message trailer with\npercent-decode (metadata.lisp), called from the response-handling paths\nin call.lisp.  The decoder assumes a \"%\" is always followed by two hex\ndigits:\n\n    ((and (char= char #\\%) (<= (+ i 2) len))\n     (vector-push-extend (parse-integer string :start (1+ i)\n                                                :end (+ i 3) :radix 16)\n                         bytes)\n     (incf i 3))\n\nA malicious or buggy server can send a grpc-message value with a\ntruncated escape (for example \"...%2\", one hex digit before end-of-\nstring) or a non-hex escape (\"...%ZZ\").  In the truncated case\nparse-integer is called with :end past the end of the string, raising\nSB-KERNEL:BOUNDING-INDICES-BAD-ERROR; in the non-hex case parse-integer\nraises on the invalid digits.  Either uncaught error aborts processing\nof that RPC's response.\n\nImpact is limited: the attacker must be the server the client chose to\ntalk to (or be positioned to tamper with its trailers), and the effect\nis confined to the availability of the client's own in-flight call, not\nother calls or other clients -- hence the high attack complexity and low\nseverity.",
    "recommendation": "Make percent-decode tolerant of malformed peer input.  The working-tree\nfix only treats \"%XX\" as an escape when two hex digits actually follow\n(using digit-char-p ... 16 and requiring (<= (+ i 3) len)); otherwise it\nemits the \"%\" literally instead of calling parse-integer.  This never\nsignals on malformed input while preserving correct decoding of\nwell-formed \"%XX\" sequences.  Callers that parse server-supplied status\ntrailers (e.g. the grpc-status parse-integer in call.lisp) should\nadditionally be wrapped to default to INTERNAL on bad input."
  },
  {
    "id": "CL-SEC-2026-0211",
    "title": "Backslash protocol-relative URL bypass in cl-sanitize-html allows resource loading from arbitrary domains",
    "reported": "2026-06-22",
    "published": "2026-06-22",
    "severity": "medium",
    "cvss": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:N/A:N",
    "cvss-score": "4.1",
    "cwe": "CWE-20",
    "project-name": "cl-sanitize-html",
    "homepage": "https://github.com/atgreen/cl-sanitize-html",
    "introduced-commit": "a93616f",
    "introduced-url": "https://github.com/atgreen/cl-sanitize-html/commit/a93616f",
    "fixed-commit": "6427288",
    "fixed-version": "1.0.1",
    "fixed-url": "https://github.com/atgreen/cl-sanitize-html/commit/6427288",
    "cve-id": null,
    "status": "fixed",
    "description": "This is an incomplete-fix follow-on to CL-SEC-2026-0132.\n\nThe protocol-allowed-p function (src/policies.lisp) permits relative\nURLs by accepting any value whose first character is \"/\", and -- after\nthe CL-SEC-2026-0132 fix (commit 10cc58c, v1.0.0) -- rejecting\nprotocol-relative URLs that begin with \"//\". However, that fix only\nspecial-cases the forward slash:\n\n    (and (> (length url-lower) 0)\n         (char= (char url-lower 0) #\\/)\n         (or (<= (length url-lower) 1)\n             (char/= (char url-lower 1) #\\/)))\n\nPer the WHATWG URL Standard, a backslash is treated as equivalent to a\nforward slash in the authority position of special-scheme (http/https)\nURLs. Browsers therefore resolve \"/\\evil.com/x\", \"\\/evil.com\", and\n\"\\\\evil.com\" as the network-path (protocol-relative) reference\n\"//evil.com/x\" -> \"https://evil.com/x\". Because the sanitizer only\nrejects a literal \"//\", the value \"/\\evil.com/...\" passes the relative-\nURL allowance (second character is \"\\\", not \"/\") and is preserved on\nhref, src, cite, poster, background, and srcset attributes. The\nattacker-controlled domain is then loaded by the victim's browser.\n\nAs with CL-SEC-2026-0132, this enables tracking pixels and IP/data\nexfiltration via auto-loaded <img src> (no user interaction required),\nand open-redirect / phishing via <a href> on click. The bypass affects\nthe default policy; no special configuration is needed. The entity-\nencoded form (e.g. \"/&#92;evil.com\") is normalized to the same payload\nand is likewise affected.\n\nConfirmed against the built library (SBCL 2.6.5, plump 20260218):\n\n    (protocol-allowed-p *default-policy* \"//evil.com/x\")  => NIL  ; 0132\n    (protocol-allowed-p *default-policy* \"/\\\\evil.com/x\")  => T    ; bypass\n\n    (sanitize-html \"<img src='/\\\\evil.com/track.gif'>\")\n      => \"<img src=\\\"/\\\\evil.com/track.gif\\\">\"",
    "recommendation": "Update to cl-sanitize-html 1.0.1 or later. The fix treats the backslash\nas equivalent to the forward slash before the relative-URL check: a URL\nis rejected whenever its first two characters are each a path separator\nin the set { \"/\", \"\\\" } -- i.e. //, /\\, \\/, and \\\\ are all protocol-\nrelative -- so a relative URL is accepted only when it starts with a\nsingle separator that is not immediately followed by another separator."
  }
]