Repository navigation
fix(server): reject ill-formed UTF-8 in inbound frames with 1007 - #390
Conversation
RFC 6455 closes a connection with 1007 on a text frame or close reason that is not valid UTF-8. The inbound check takes any lead byte from C0 to FF with the right number of continuation bytes, so it lets through overlong forms, UTF-16 surrogates, code points above U+10FFFF and the bytes F5 to FF. Seven text-frame cases and one close-reason case fail on main. A case with the first and last code point of each UTF-8 length passes on main and must keep passing. Part of #385 _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_
The inbound check took any lead byte from C0 to FF with the right number of continuation bytes. A text frame or close reason with an overlong form, a UTF-16 surrogate, a code point above U+10FFFF or a byte F5 to FF passed, and the plugin decoded and ran the request. RFC 6455 requires the endpoint to close such a connection with 1007. is_valid_utf8 now walks the text with utils.utf8_sequence_length, the decoder behind the outbound valid_utf8, so both directions share one definition of UTF-8. It is as fast as the old loop on 10 MB of ASCII and faster on multibyte text. Part of #385 _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_
…check Only server/utils.lua changed. A stricter frame check closes the connection of a client that sends ill-formed UTF-8, which reaches diffs through the same on_disconnect path as any other close. The model already lets a client disconnect at any step, and check.sh passes. Part of #385 _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_
|
@codex review |
|
@codex security review |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Codex Review: Didn't find any major issues. Bravo. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
🛡️ Codex Security ReviewSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
…, #388, #390 The merge keeps this branch's bulk-close cases on main's ordered bulk_closes list (#386). Main changed selection.lua, utils.lua, and visual_commands.lua, which cannot reach diff state. check.sh passes on the merged tree. Part of #363 _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_
Why
The operator goal is to clear the open follow-up issues in this repository (#385 now) with small reviewed PRs. This PR takes the inbound part of #385 item 6. Items 7 and 8 are in #386.
RFC 6455 requires an endpoint to close a connection with 1007 when a text frame or a close reason is not valid UTF-8. The plugin's inbound check,
server/utils.luais_valid_utf8, took any lead byte from C0 to FF with the right number of continuation bytes. So it let through overlong forms, UTF-16 surrogates, code points above U+10FFFF, and the bytes F5 to FF. The plugin then decoded the request and ran it. The outbound side already has a strict decoder,claudecode.utils.utf8_sequence_length(behindvalid_utf8, since #378). This PR makes the inbound check use that decoder.The outbound part of item 6 does not change: a buffer name with invalid bytes still reaches Claude as U+FFFD, as
lua/claudecode/server/README.mddocuments.What changed
lua/claudecode/server/utils.lua:is_valid_utf8walks the text withutils.utf8_sequence_lengthand returnsfalseat the first ill-formed sequence.frame.parse_frame(text frames and close reasons) andframe.validate_framecall it as before.tests/unit/server/frame_spec.lua: seven text-frame cases and one close-reason case that must get 1007, and one frame with the first and last code point of each UTF-8 length that must still parse.lua/claudecode/server/README.md: the 1007 invariant names what counts as ill-formed..agents/skills/verify-diff-lifecycle/covered.lock: re-recorded. Only theserver/utils.luafile hash changed, with no covered block or call site. A rejected frame closes the connection through the sameon_disconnectpath as any other close, and the model already lets a client disconnect at any step.Scope
#385 item 6, inbound only. No change to what the plugin sends.
Blast Radius
A client that sends ill-formed UTF-8 now gets Close 1007, where the plugin used to run its request. Claude Code cannot send such bytes: it serializes each message with
JSON.stringify, which escapes a lone surrogate as\uD800, and its WebSocket sends a JS string as UTF-8. The real CLI run below shows that its non-ASCII text still goes through.Verification
busted tests/unit/server/frame_spec.lua(red)mise run test: 908 successes, 0 failuresverify-claudecode-nvimharness)ED A0 80,C0 AF,F4 90 80 80andF5 80 80 80each get agetDiagnosticsanswer. The plugin ran the call with the ill-formed pathClose frame, code 1007. A frame withF0 9F 98 80(😀) still gets its answer, and the plugin keeps its other clientclaude -p --model haikuwith the plugin as an MCP server over the CLI's own WebSocket transport, asked to callgetDiagnosticsforfile://…/naïve-日本-😀.txtgetDiagnostics arguments: {uri=50 bytes}and finds the open buffer. Claude repliesünïcode ✓ diagnostic, the diagnostic set on that buffer. The CLI closes with 1000uri=50 bytes, replyünïcode ✓ diagnostic, close 1000is_valid_utf8in LuaJIT 2.1, best of 3verify-diff-lifecycle/check.shPASS.main: pass (89205 distinct states found),live: pass (5269 distinct states found)How the real CLI run works
claude -p --idedoes not connect to an IDE in print mode. So the script hands the CLI an--mcp-configfile with an MCP server of typewsat the plugin's port. The lock file's token is in thex-claude-code-ide-authorizationheader. The CLI's IDE transport (ws-ide) and this one build the sameWebSocket(url, { protocols: ["mcp"], headers })and the same transport class, so the bytes on the wire come from the same code. The run uses its ownHOMEandCLAUDE_CONFIG_DIRunder the harness state dir, andANTHROPIC_API_KEYfor one haiku request. The 50-byteuriis the UTF-8 length offile:///tmp/vcn.<id>/work/naïve-日本-😀.txt.Part of #385
Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:xhigh