Skip to content

Fix crash when a server sends an x-hex-message header#1180

Merged
ericmj merged 3 commits into
mainfrom
fix-x-hex-message-binary
Jun 16, 2026
Merged

Fix crash when a server sends an x-hex-message header#1180
ericmj merged 3 commits into
mainfrom
fix-x-hex-message-binary

Conversation

@ericmj

@ericmj ericmj commented Jun 16, 2026

Copy link
Copy Markdown
Member

Hex.HTTP.handle_hex_message/1 is meant to surface x-hex-message headers from the server (e.g. deprecation notices) as API warning: / API error: output. It runs :binary.list_to_bin/1 on the header value, which requires a charlist — but decode_header/1 converts every response header value to a binary via List.to_string/1. So any real x-hex-message header raises ArgumentError: not an iodata term inside the HTTP task and fails the request (e.g. mix deps.get).

No hex.pm endpoint currently sends x-hex-message, so the bug has been latent — the existing test only exercised the function with charlists. This switches to IO.iodata_to_binary/1, which accepts both binaries (the real path) and charlists (the existing test path), and adds a binary regression case.

Response headers are decoded to binaries, but handle_hex_message/1 ran
:binary.list_to_bin/1 on the value, which requires a charlist and raised
"not an iodata term", failing the request. Use IO.iodata_to_binary/1,
which accepts both binaries and charlists.
@maennchen

Copy link
Copy Markdown
Member

I think this bug was a blessing in disguise. This would also be called for untrusted servers, right?

There's quite a lot of terminal escape sequences that could potentially be problematic. We should add some escaping.

@maennchen

Copy link
Copy Markdown
Member

Something along those lines:

defmodule TerminalSafe do
  @bidi_controls [
    0x061C,
    0x202A,
    0x202B,
    0x202C,
    0x202D,
    0x202E,
    0x2066,
    0x2067,
    0x2068,
    0x2069
  ]

  def escape(binary) when is_binary(binary) do
    binary
    |> String.to_charlist()
    |> Enum.map_join(fn
      cp when cp in @bidi_controls ->
        "\\u{" <> Integer.to_string(cp, 16) <> "}"

      ?\n ->
        "\n"

      ?\t ->
        "\t"

      # Important terminal controls
      ?\e ->
        "\\e"

      ?\r ->
        "\\r"

      ?\b ->
        "\\b"

      ?\a ->
        "\\a"

      # Other C0 controls
      cp when cp < 0x20 ->
        hex =
          cp
          |> Integer.to_string(16)
          |> String.upcase()
          |> String.pad_leading(2, "0")

        "\\x" <> hex

      # DEL
      0x7F ->
        "\\x7F"

      # C1 controls
      cp when cp >= 0x80 and cp <= 0x9F ->
        "\\u{" <> Integer.to_string(cp, 16) <> "}"

      # Normal printable codepoint
      cp ->
        <<cp::utf8>>
    end)
  end
end

@ericmj ericmj force-pushed the fix-x-hex-message-binary branch from 23a9dc8 to d3770a4 Compare June 16, 2026 18:48
Server-provided x-hex-message values are printed to the terminal, so an
untrusted server could embed ANSI escapes or bidirectional overrides.
Escape control characters before printing while keeping printable text,
newlines and tabs intact.
@ericmj ericmj merged commit 3de1df3 into main Jun 16, 2026
22 checks passed
@ericmj ericmj deleted the fix-x-hex-message-binary branch June 16, 2026 19:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants