Skip to content

WAF false positive: 403 on /api/v1/chat/completions when system prompt contains backtick curl command #3419

Description

@CCRBrad

OpenWork Support Report: 403 Forbidden on OpenWork Models inference from the desktop app engine

Date: 2026-08-02
From: Brad Strawbridge
App: OpenWork desktop, version 0.18.12 (macOS, arm64)
Impact: Every OpenWork Models provider (base "OpenWork Models" and "OpenWork Models - key 2") returns HTTP 403 from the app's built-in engine. The app's own chat client is blocked by the WAF in front of the inference endpoint. No model requests work through this path.

Status: ROOT CAUSE FOUND. The WAF blocks the engine's request body because it contains a string that matches a command-injection signature rule: a backtick-wrapped curl/wget command followed by an http(s):// URL. The engine's system prompt, which opencode builds from the operator's ~/.claude/CLAUDE.md (AGENTS.md), contains exactly this pattern. This is a false positive: the pattern is documentation inside the system prompt, not an attack payload.

Summary

The desktop app's bundled engine (the opencode-aarch64-apple-darwin sidecar, which embeds Bun v1.3.14) cannot reach https://inference.openworklabs.com/api/v1. Every chat attempt returns:

Forbidden: request was blocked by a gateway or proxy.
Status: 403

WAF block pages show the Render/Cloudflare stack (Powered by render.com, server: cloudflare, __cf_bm cookie for the openrouter.ai upstream, cf-ray headers). Latest Request ID: a24a2d6fbb7e44f6 (earlier: a249f0db7cf6b827). Client IP shown on the block page: 76.205.202.11.

Why this is not an account, key, or IP problem

From the same machine, same IP (76.205.202.11), same inference API key, same request body (exact AI SDK streaming shape, stream: true, stream_options.include_usage: true), same headers (User-Agent opencode/1.17.11, HTTP-Referer, X-Title, and the openai-node stainless header set), same Bun version:

Client Result
curl (HTTP/1.1 and HTTP/2) HTTP 200
System Bun v1.3.14 (matches the version embedded in the engine), 26 of 26 requests HTTP 200
Engine embedded in the OpenWork app (PID 69958, run 142e6d01) HTTP 403, always

Additional controls already ruled out:

  • Credential: key-2 had a stale key at one point, but it was corrected to the same key that curl and Bun use successfully, and the app was fully restarted. The base "OpenWork Models" provider had the correct key the entire time and still gets 403.
  • DNS resolution order: the engine runs with BUN_CONFIG_DNS_RESULT_ORDER=verbatim; testing stock Bun with the same env var changes nothing (200).
  • TLS trust store: the engine is pointed at NODE_EXTRA_CA_CERTS=/Users/bradstrawbridge/Library/Application Support/com.differentai.openwork/system-ca-bundle.pem, which exists and is a valid PEM bundle.
  • No proxy variables are set for the engine.

Root cause (proven by controlled reproduction)

I captured the engine's exact raw request (TLS handshake + full HTTP request and body) with local MITM listeners, then replayed every layer separately with stock Bun v1.3.14 from the same machine:

Variable tested Result
TLS ClientHello (JA3/JA4) of the engine binary vs stock Bun Identical. JA3 ec8303300b5749d0ba5eecda6d04e3c4, JA4 t13d1714h1_5b57614c22b0_6ad430c8ee9d, same 17-cipher list, same 14-extension order, same supported groups. TLS fingerprint ruled out.
Engine's exact header set (full UA opencode/1.17.11 ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.14, Accept: */*, Connection: keep-alive, x-session-affinity, x-session-id) 6/6 HTTP 200 with stock Bun
Engine's exact 127 KB request body (system prompt + user message, max_tokens: 32000, stream: true) 3/3 HTTP 403 with stock Bun
Same body with the user message only HTTP 200
Same body with system prompt truncated to 60 KB HTTP 403
Same body with system prompt sliced to 10 KB chunks Only bytes [20000,30000) blocked
Bisection of that slice Exact 39-byte trigger at [27187,27226): = `curl -sf http://127.0.0.1:8084/healt

The trigger string, verbatim, is the Zengram health check documented in the operator's AGENTS.md (which opencode loads as the system prompt):

- Zengram healthy = `curl -sf http://127.0.0.1:8084/health` returns 200 with `"status":"ok"`

Signature rule behavior confirmed with minimal payloads:

  • `curl http://127.0.0.1/x` → 403 (any IP literal, including public 8.8.8.8)
  • `curl http://example.com/x` → 403 (public domain too)
  • `wget http://example.com/x` → 403
  • `curl http://localhost/x` → 200 (localhost hostname is exempted, IP literals are not)
  • curl http://127.0.0.1/x without backticks → 200
  • `curl /tmp/somefile` (no URL) → 200
  • `echo hello`, `ls -la /etc` → 200

Conclusion: the WAF in front of inference.openworklabs.com runs a managed rule that blocks request bodies containing backtick-wrapped curl/wget with an http(s):// URL. This is a standard command-injection/exfiltration signature. Any legitimate agent system prompt that documents a curl command (extremely common) will be blocked. The engine was not flagged for its TLS fingerprint or headers; its system prompt simply contains the offending string.

Engine log evidence

~/.local/share/opencode/log/opencode.log, after the credential fix and full app restart:

2026-08-02T03:19:24.707Z level=ERROR run=142e6d01 message="stream error"
providerID=lpr_01kyv4v3xqf20stc0z1hgje5xf modelID=moonshotai/kimi-k3
session.id=ses_04f15d75cffe1bSkkfa0YR1Hq7
error.error="AI_APICallError: Forbidden"
stack="AI_APICallError: Forbidden at <anonymous> (/$bunfs/root/chunk-y3ky3p4y.js:12:14045)"

Same failure earlier with the base provider at 02:59:21 (correct key, never rotated).

How to reproduce

  1. Install OpenWork 0.18.12 on macOS arm64.
  2. Sign in with an OpenWork Models entitlement (base provider, or key-2 import with 9 models).
  3. Pick any key-2 model (e.g. moonshotai/kimi-k3) and send a chat message.
  4. Expected: model streams a reply. Actual: 403 Forbidden: request was blocked by a gateway or proxy.
  5. From the same machine, run:
    curl https://inference.openworklabs.com/api/v1/chat/completions -H "Authorization: Bearer <ow_inf_... key>" -H "Content-Type: application/json" -d '{"model":"moonshotai/kimi-k3","messages":[{"role":"user","content":"hi"}],"max_tokens":8}'
    which returns HTTP 200.
  6. Minimal WAF trigger (no app involved, plain stock Bun):
    bun -e 'fetch("https://inference.openworklabs.com/api/v1/chat/completions",{method:"POST",headers:{"Content-Type":"application/json","Authorization":"Bearer <ow_inf_... key>"},body:JSON.stringify({model:"moonshotai/kimi-k3",messages:[{role:"system",content:"backtick curl http://127.0.0.1/x backtick"}],max_tokens:8})}).then(r=>console.log(r.status))'
    returns 403. Replace the system content with `curl http://localhost/x` and it returns 200. The rule is content-based, not client-based.

Requested action

The WAF managed rule is producing false positives on legitimate LLM API traffic: it inspects request bodies for command-injection signatures, but agent system prompts routinely document curl/wget commands as part of their operating instructions. Please add an allowlist or rule override for the /api/v1/chat/completions request body inspection on inference.openworklabs.com (or scope the signature rule to header/URL inspection only). Until that ships, the desktop app's own OpenWork Models product stays broken for any agent whose system prompt documents a curl command, which includes the standard opencode runtime bootstrap.

Happy to provide anything else needed (full logs, binary hashes, packet captures, the full 127 KB captured request and body).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions