security: flag condition leaks in http responses

Jaime Fournier

03814892333e1d014900466ab70f86f382231e29

diff --git a/data/security-rules.sexp b/data/security-rules.sexp
index 1446bc4..17aa7f4 100644
--- a/data/security-rules.sexp
+++ b/data/security-rules.sexp
@@ -1101,6 +1101,15 @@
      "\\(string-(?:contains|prefix)\\?\\s+(?:resp|response|headers?)[\\w-]*\\s+")
    ("scope" . "scheme")
    ("severity" . "high"))
+ (("id" . "http-response-raw-condition-message")
+   ("message"
+     .
+     "A raw condition message is being written into an HTTP response. Route handler failures through (std security errors), return a generic client-safe message plus opaque reference, and log full condition details only on the server side.")
+   ("pattern"
+     .
+     "\\((?:respond-text|respond-html|respond-json|respond|make-response|ring-response|rack-response)[\\s\\S]{0,240}\\((?:condition-message|display-condition)\\b|\\((?:condition-message|display-condition)\\b[\\s\\S]{0,240}\\((?:respond-text|respond-html|respond-json|respond|make-response|ring-response|rack-response)\\b")
+   ("scope" . "scheme")
+   ("severity" . "high"))
  (("id" . "shell-injection-format-tilde-s")
    ("message"
      .
diff --git a/docs/kimi3-security-recommmendations.md b/docs/kimi3-security-recommmendations.md
index be7b31f..0b2e141 100644
--- a/docs/kimi3-security-recommmendations.md
+++ b/docs/kimi3-security-recommmendations.md
@@ -778,9 +778,10 @@ they are build-time. Close the runtime loop.
   redacted from both JSONL and human summaries. `(std net grpc)` handler
   failures now log internal details server-side and return client-visible
   opaque refs; `(std net thread-httpd)` does the same for handler exceptions.
-  Remaining work: router integrations plus MCP/LSP/repl-protocol surfaces need
-  opaque client error refs, the scanner needs a condition-message HTTP-response
-  rule, and release artifact path-leak checks still need a gate.
+  `jerboa_security_scan` now flags raw `condition-message`/`display-condition`
+  values written into HTTP response constructors. Remaining work: router
+  integrations plus MCP/LSP/repl-protocol surfaces need opaque client error
+  refs, and release artifact path-leak checks still need a gate.
 
 ### K3-P1-11 — Parameterized-only SQL in the safe surface
 **Serves:** G1. **Effort:** 2–3 days.
diff --git a/docs/security-reference.md b/docs/security-reference.md
index 1ca0260..4c73c50 100644
--- a/docs/security-reference.md
+++ b/docs/security-reference.md
@@ -207,6 +207,14 @@ text, and reason. New sites fail audit until converted to `jerboa-read` /
 justification, or added to the manifest with a reviewed provenance reason.
 Stale manifest entries also fail audit.
 
+### Security Scanner Rules
+
+`jerboa_security_scan` loads `data/security-rules.sexp`. The rule set includes
+`http-response-raw-condition-message`, which flags raw `condition-message` or
+`display-condition` values being written into HTTP response constructors.
+Protocol handlers should return a generic client-safe message plus an opaque
+reference from `(std security errors)` and log full details server-side.
+
 ### Usage
 
 ```scheme
diff --git a/mcp/server.ss b/mcp/server.ss
index 2073fd4..986a1e5 100644
--- a/mcp/server.ss
+++ b/mcp/server.ss
@@ -2529,6 +2529,7 @@
     "direct-chezscheme-import-user-code"
     "do-loop-unchecked-bound"
     "ffi-u8-star-for-foreign-alloc"
+    "u8star-ffi-with-foreign-alloc"
     "foreign-alloc-no-free"
     "fork-without-child-limit"
     "load-shared-object-crash-static"
@@ -2635,7 +2636,8 @@
     [(string=? rule-id "do-loop-unchecked-bound")
      (or (not (source-call-token? line "(do"))
          (nonnegative-do-bound? line))]
-    [(string=? rule-id "ffi-u8-star-for-foreign-alloc")
+    [(or (string=? rule-id "ffi-u8-star-for-foreign-alloc")
+         (string=? rule-id "u8star-ffi-with-foreign-alloc"))
      (or (not (string-contains content "foreign-alloc"))
          (not (ffi-output-pointer-context? line)))]
     [(string=? rule-id "foreign-alloc-no-free")
diff --git a/mcp/test/protocol-test.ss b/mcp/test/protocol-test.ss
index 23e36ab..d0eaa2a 100644
--- a/mcp/test/protocol-test.ss
+++ b/mcp/test/protocol-test.ss
@@ -159,6 +159,8 @@
 (def verify-bad-file (path-join "/tmp" "jmcp-bad.ss"))
 (def security-context-safe-file (path-join "/tmp" "jmcp-security-safe.ss"))
 (def security-context-unsafe-file (path-join "/tmp" "jmcp-security-unsafe.ss"))
+(def security-http-condition-file
+  (path-join "/tmp" "jmcp-http-condition-leak.ss"))
 (def security-project (make-temporary-file-name "jmcp-security-project"))
 (def security-project-profile-dir (path-join security-project ".jerboa"))
 (def security-project-profile-file
@@ -202,6 +204,9 @@
                  "#\\escape\n"))
 (write-file-string variadic-file "(import (jerboa prelude))\n(def c-fcntl (foreign-procedure \"fcntl\" (int int int) int))\n")
 (write-file-string
+  security-http-condition-file
+  "(import (jerboa prelude) (std net thread-httpd))\n(def (handler req)\n  (guard (e [else (respond-text 500 (condition-message e))])\n    (error 'handler \"secret\")))\n")
+(write-file-string
  chez-reader-file
  (string-append "#!chezscheme\n(library (tmp colon-reader) (export "
                 (string #\: #\?)
@@ -810,9 +815,13 @@
                 (list (cons "symbol" "file-length"))))
     (call-tool 134 "jerboa_feature_manifest_audit"
                (alist->hash-table
-                (list (cons "features_path" features-file)))))))
+                (list (cons "features_path" features-file))))
+    (call-tool 135 "jerboa_security_scan"
+               (alist->hash-table
+                (list (cons "file_path" security-http-condition-file)
+                      (cons "severity_threshold" "high")))))))
 
-(check "expected 134 responses" (= (length responses) 134))
+(check "expected 135 responses" (= (length responses) 135))
 
 (def init-result (result (car responses)))
 (check "initialize server name"
@@ -1224,6 +1233,10 @@
 (check "direct security scan honors profile suppression when project supplied"
        (string-contains
         (content-text (result (list-ref responses 127))) "no findings"))
+(check "security scan catches condition messages in http responses"
+       (string-contains
+        (content-text (result (list-ref responses 134)))
+        "http-response-raw-condition-message"))
 
 (check "tool error formatting includes tool name and condition"
        (and (hash-ref (result (list-ref responses 108)) "isError")