Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ All notable changes to this project are documented here. The format follows

## [Unreleased]

## [0.10.1] - 2026-09-23
## [0.10.1] - 2026-09-26

A security release. Found by the project's own security review (three independent review
lanes, their findings reproduced before any fix). No public entry added, removed, renamed,
Expand Down Expand Up @@ -45,7 +45,9 @@ patch: `~> 0.10.0` already admits it. `beam_mcp_signer` `0.2.1` is released besi
in the host's dispatch.
- How the subset reads, stated in `BeamMCP.Schema`'s moduledoc: `pattern` uses ECMA-262's
character classes and anchors (`\d`, `\w`, `\s` ASCII; `$` never before a trailing
newline), so `^[a-z]+$` refuses `"abc\n"`; `enum`, `const` and `uniqueItems` compare as JSON
newline), so `^[a-z]+$` refuses `"abc\n"`; on OTP 27 only, `\w`, `\b` and the POSIX letter
classes also match the Latin-1 letters U+00AA to U+00FF, since that release's `:re` has
Latin-1 tables and no ASCII ones; `enum`, `const` and `uniqueItems` compare as JSON
(`1` equals `1.0`); `integer` refuses `1.0`, stricter than JSON Schema, so a host typed for
integers never receives a float. A property name that is not a string, and an `enum` or
`const` value that is not JSON, are refused when the catalog is loaded.
Expand Down
8 changes: 7 additions & 1 deletion lib/beam_mcp/schema.ex
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ defmodule BeamMCP.Schema do
classes and anchors (`\\d`, `\\w` and `\\s` are ASCII, `$` matches only at the end, never
before a trailing newline), so an allowlist pattern admits what a client's own validator
would; constructs PCRE and ECMA-262 read differently beyond those are the host's to avoid.
One exception, by OTP release: on OTP 27, whose `:re` is PCRE with Latin-1 character
tables and offers no ASCII tables, `\\w`, `\\b` and the POSIX letter classes
(`[[:alpha:]]` and the like) also match the Latin-1 letters U+00AA to U+00FF (`é`, `ß`);
`\\d`, `\\s` and `$` read as above there too (measured). From OTP 28 (PCRE2) all of them
are ASCII. A host on OTP 27 that means ASCII letters writes `[A-Za-z0-9_]`.
`enum`, `const` and `uniqueItems` compare as JSON does: `1` and `1.0` are the same value.

**Any other keyword is refused, not ignored**: a schema using `oneOf`, `$ref`, `if`, or
Expand All @@ -48,7 +53,8 @@ defmodule BeamMCP.Schema do
@type result :: :ok | {:error, String.t()}

# PCRE with ECMA-262's classes and anchors: `:unicode` without `:ucp` keeps `\d`, `\w` and
# `\s` ASCII, and `:dollar_endonly` keeps `$` from matching before a trailing newline.
# `\s` ASCII (on OTP 27, `\w` also takes the Latin-1 letters; the moduledoc says so), and
# `:dollar_endonly` keeps `$` from matching before a trailing newline.
@pattern_options [:unicode, :dollar_endonly]

@enforced [
Expand Down
12 changes: 11 additions & 1 deletion test/beam_mcp/arguments_schema_and_faults_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,17 @@ defmodule BeamMCP.ArgumentsSchemaAndFaultsTest do

assert {:error, _} = BeamMCP.Schema.validate(%{"a" => "abc\n"}, only.("^[a-z]+$"))
assert {:error, _} = BeamMCP.Schema.validate(%{"a" => "٣"}, only.("^\\d$"))
assert {:error, _} = BeamMCP.Schema.validate(%{"a" => "é"}, only.("^\\w$"))
assert {:error, _} = BeamMCP.Schema.validate(%{"a" => "\u00a0"}, only.("^\\s$"))

# \w is ASCII from OTP 28 (PCRE2). OTP 27's :re is PCRE with Latin-1 tables and offers no
# ASCII ones, so there \w also takes U+00AA to U+00FF, as the moduledoc states; nothing
# outside Latin-1 on either (measured on 27, 28 and 29).
assert {:error, _} = BeamMCP.Schema.validate(%{"a" => "Ā"}, only.("^\\w$"))

if String.to_integer(System.otp_release()) >= 28,
do: assert({:error, _} = BeamMCP.Schema.validate(%{"a" => "é"}, only.("^\\w$"))),
else: assert(:ok = BeamMCP.Schema.validate(%{"a" => "é"}, only.("^\\w$")))

assert :ok = BeamMCP.Schema.validate(%{"a" => "abc"}, only.("^[a-z]+$"))
assert :ok = BeamMCP.Schema.validate(%{"a" => "é"}, only.("^.$"))
end
Expand Down
Loading