Skip to content

v0.10.1

Latest

Choose a tag to compare

@mikecarroll mikecarroll released this 26 Oct 15:26
1b6dd67

🚀 What's New

v0.10.1 is a security and hardening release. It closes the credential-redaction and body-size gaps found in the last two release reviews: request and response bodies, streamed (SSE) responses and forward_webhook bodies are now redacted, response bodies and batch items respect max_captured_body_bytes, unsigned OpenAI webhooks are rejected in development and test when a secret is set, and Amazon Bedrock FIPS, dual-stack and China endpoints are captured. It also contains one breaking rename (public_content_service, below), so please read the upgrade notes.


✨ New: Amazon Bedrock FIPS, dual-stack and China endpoints

The default intercept list held only bedrock-runtime.*.amazonaws.com. Calls to bedrock-runtime-fips.<region>.amazonaws.com (the usual choice for GovCloud), the dual-stack IPv6 hosts bedrock-runtime.<region>.api.aws and bedrock-runtime-fips.<region>.api.aws, and the China partition bedrock-runtime.<region>.amazonaws.com.cn were silently not captured. They are now. (A VPC interface endpoint host still needs its own entry in intercept_addresses.)


🔒 Security Hardening

  • Credentials in request and response bodies are redacted. String values under authorization, authorization_token, access_token, refresh_token, id_token, api_key, client_secret, private_key, secret_key, password, x-api-key, secret, aws_secret_access_key, session_token and bearer_token (any depth, exact name) become [REDACTED], as do MCP tool headers and Realtime ephemeral keys. response_body is now redacted too, not just request_body. String values under url, server_url, image_url, file_url and endpoint that are a single absolute URL go through the URL sanitizer. Bare token is left alone so logprobs keep working. A secret embedded in a URL path cannot be detected and is sent as is. See Body Redaction.
  • Streamed (SSE) responses and forward_webhook bodies are redacted. The JSON object or array on each data: line, JSON text passed as a string, and the request_body / response_body given to Coolhand.logger_service.forward_webhook are parsed, redacted and re-serialized. [DONE], event and comment lines and plain text are untouched; a payload that cannot be redacted is replaced by a redaction_failed marker rather than logged unchecked.
  • Unsigned OpenAI webhooks are rejected in development and test when a secret is set. Previously a request missing the signature headers passed in those environments, so an app running with RAILS_ENV=development on a reachable host accepted forged batch.completed events. A nil payload is now rejected as well. With no secret configured, development and test still skip verification with a warning.
  • WebhookValidator replay protection: webhook ids are remembered for twice the tolerance window (closing a replay gap at the edge of the window), and the claim is released when your process_event raises, so OpenAI's retry of the same webhook-id is processed instead of rejected.
  • OpenAi::BatchResultProcessor validates ids before use. The batch id and input/output file ids are format-checked before any OpenAI API call made with your key, and a batch with missing timestamps is skipped instead of logged with epoch-0 times. Batch logs now carry source_api: "openai", the model, and an absolute https://api.openai.com/... URL.
  • forward_webhook no longer forwards the whole Rack env. Passing request.headers shipped session and request data (rack.session, action_dispatch.*) and CGI variables such as QUERY_STRING. When the input looks like a whole Rack env, only HTTP_*, CONTENT_TYPE and CONTENT_LENGTH are forwarded, so process env vars such as DATABASE_URL are dropped too. A caller-supplied url: is sanitized.
  • URL and header redaction gaps closed. The query params code, sas, pwd, passwd, pass, sid, otp, jwt, bearer, session, cookie, passphrase, ticket and assertion are redacted; ;name=value matrix params and bare query segments are handled; URL fragments (#access_token=...) are dropped. Rate-limit headers (x-ratelimit-remaining-tokens, ...) are no longer over-redacted. Invalid UTF-8 in a URL no longer drops the log.
  • Path matching is normalized. Repeated /, ./.. segments and unreserved percent-escapes are normalized before exclude_api_patterns and path-anchored intercept_addresses matching, so /openai//files or /openai/%66iles can no longer slip past an exclusion. The URL that is sent and logged is unchanged.
  • Streaming capture: a host streaming block that raises no longer leaks its message into the log, and a non-intercepted request made inside a streaming block no longer contributes its chunks to the outer request's log.
  • Webhook helper methods are private. intercept_batch_request, webhook_secret and process_event are no longer public in the controller, so they cannot be routed to as actions.

🛠️ Fixed: body size caps and capture failures

  • max_captured_body_bytes now caps response bodies (streamed or not) as well as requests. A response over the cap (default 1 MB) is logged as a body_too_large marker; your app always receives the full response.
  • Batch items and chunked request bodies respect the cap. The OpenAI and Vertex batch processors cap each item, OpenAI result files are read line by line, and a malformed line is skipped instead of dropping the file. A chunked body_stream request is no longer read ahead of the send.
  • A failure while recording a response or a streamed chunk is logged and swallowed instead of raising into your already-successful request. JSON nested more than 100 levels is logged as a marker instead of the raw string.

⚠️ Heads-up When Upgrading

  • Breaking: Coolhand.public_content_service is now Coolhand.help_center_service, and Coolhand::PublicContentService is now Coolhand::HelpCenterService. The old names are removed with no alias. Signatures and requests are unchanged:

    help_center = Coolhand.help_center_service # was Coolhand.public_content_service
  • Unsigned webhook tests now get a 401 in development and test if a secret is set. Sign the payload (snippet in the OpenAI webhook docs), leave the secret unset in development, or replay a real delivery.

  • Webhook helpers are private. Define webhook_secret under private in your controller; calling intercept_batch_request, webhook_secret or process_event from outside it now raises NoMethodError, and those three names are reserved.

  • Logged data changes: credential-named fields in bodies, SSE streams and forwarded webhooks, and URL fragments and more query params, now show [REDACTED] or are dropped; responses over 1 MB appear as a marker; forward_webhook headers no longer include Rack/CGI entries (remote-addr, query-string, ...). A plain header hash whose keys are all uppercase ({"ETAG" => ...}) is treated as a Rack env and its non-HTTP_* entries are dropped.

  • Custom webhook_id_store implementations now receive 2 * webhook_replay_tolerance_seconds as ttl_seconds, and may optionally implement release!(id).


🧹 Other Changes

  • Docs: docs/anthropic.md now describes the unified NetHttpInterceptor instead of the removed FaradayInterceptor; the OpenAI examples use gpt-4o-mini.
  • CI: ruby/setup-ruby bumped to 1.335.0 for the Ruby 3.3.13 / 3.4.12 / 4.0.9 regexp security fixes.
  • Test coverage gaps closed (webhook secret handling, sanitizer negatives, batch wire payloads, oversized streamed responses).

📌 Known Follow-ups

Still open: #145 absolute-URL req.path matching, and #146 test coverage gaps (its listed items are now covered, left open for a final check). Not yet tracked as issues:

  • Path matching is still case-sensitive (/OpenAI/files is not excluded), and ; matrix params and reserved escapes such as %2F are not normalized.
  • credentials is not in the body credential-key list (it can be a legitimate string), and a process env var starting with HTTP_ (other than HTTP_PROXY) still passes when forward_webhook is given a Rack env.
  • The OpenAI and Vertex batch processors' error-handling branches (background-lane rescue, missing openai gem) lack specs.

🔗 Resources