diff --git a/CHANGELOG.md b/CHANGELOG.md index 31b65b8..bd73991 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, @@ -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. diff --git a/lib/beam_mcp/schema.ex b/lib/beam_mcp/schema.ex index 3819811..48c5150 100644 --- a/lib/beam_mcp/schema.ex +++ b/lib/beam_mcp/schema.ex @@ -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 @@ -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 [ diff --git a/test/beam_mcp/arguments_schema_and_faults_test.exs b/test/beam_mcp/arguments_schema_and_faults_test.exs index f68e503..4a7cb41 100644 --- a/test/beam_mcp/arguments_schema_and_faults_test.exs +++ b/test/beam_mcp/arguments_schema_and_faults_test.exs @@ -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