Security hardening: body size limit, JSON depth limit, timing-safe auth
- Add MAX_BODY_SIZE (64KB) check before reading body — prevents pre-auth OOM on internet-facing deployments - Add JSON nesting depth limit (64 levels) — prevents C stack overflow that bypasses pcall and crashes the process - Timing-safe token comparison via XOR accumulate — prevents timing oracle on Bearer token - Auth token from LMCP_TOKEN env var (highest priority) — avoids storing token in a file readable by the read_file tool - Silent handling of unknown JSON-RPC notifications (spec compliance) - Exact path matching on /mcp endpoint (was prefix-based) - Remove dead json.array() function Findings from architecture review + security audit. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -89,6 +89,7 @@ end
|
||||
-- Decode --
|
||||
|
||||
local decode_value
|
||||
local MAX_DEPTH = 64
|
||||
local ws_chars = { [' '] = true, ['\t'] = true, ['\n'] = true, ['\r'] = true }
|
||||
|
||||
local function skip_ws(s, pos)
|
||||
@@ -143,14 +144,14 @@ local function decode_number(s, pos)
|
||||
return n, pos
|
||||
end
|
||||
|
||||
local function decode_array(s, pos)
|
||||
local function decode_array(s, pos, depth)
|
||||
pos = pos + 1 -- skip [
|
||||
local arr = {}
|
||||
pos = skip_ws(s, pos)
|
||||
if s:sub(pos, pos) == ']' then return arr, pos + 1 end
|
||||
while true do
|
||||
local val
|
||||
val, pos = decode_value(s, pos)
|
||||
val, pos = decode_value(s, pos, (depth or 0) + 1)
|
||||
arr[#arr + 1] = val
|
||||
pos = skip_ws(s, pos)
|
||||
local c = s:sub(pos, pos)
|
||||
@@ -160,7 +161,7 @@ local function decode_array(s, pos)
|
||||
end
|
||||
end
|
||||
|
||||
local function decode_object(s, pos)
|
||||
local function decode_object(s, pos, depth)
|
||||
pos = pos + 1 -- skip {
|
||||
local obj = {}
|
||||
pos = skip_ws(s, pos)
|
||||
@@ -174,7 +175,7 @@ local function decode_object(s, pos)
|
||||
if s:sub(pos, pos) ~= ':' then error('expected : at ' .. pos) end
|
||||
pos = skip_ws(s, pos + 1)
|
||||
local val
|
||||
val, pos = decode_value(s, pos)
|
||||
val, pos = decode_value(s, pos, (depth or 0) + 1)
|
||||
obj[key] = val
|
||||
pos = skip_ws(s, pos)
|
||||
local c = s:sub(pos, pos)
|
||||
@@ -184,12 +185,14 @@ local function decode_object(s, pos)
|
||||
end
|
||||
end
|
||||
|
||||
decode_value = function(s, pos)
|
||||
decode_value = function(s, pos, depth)
|
||||
depth = depth or 0
|
||||
if depth > MAX_DEPTH then error("JSON nesting too deep") end
|
||||
pos = skip_ws(s, pos)
|
||||
local c = s:sub(pos, pos)
|
||||
if c == '"' then return decode_string(s, pos)
|
||||
elseif c == '{' then return decode_object(s, pos)
|
||||
elseif c == '[' then return decode_array(s, pos)
|
||||
elseif c == '{' then return decode_object(s, pos, depth)
|
||||
elseif c == '[' then return decode_array(s, pos, depth)
|
||||
elseif c == 't' then
|
||||
if s:sub(pos, pos + 3) == 'true' then return true, pos + 4 end
|
||||
elseif c == 'f' then
|
||||
@@ -210,9 +213,5 @@ end
|
||||
-- Sentinel for JSON null
|
||||
json.null = setmetatable({}, { __tostring = function() return 'null' end })
|
||||
|
||||
-- Helper: encode a table as a JSON array even if empty
|
||||
function json.array(t)
|
||||
return setmetatable(t or {}, { __is_array = true })
|
||||
end
|
||||
|
||||
return json
|
||||
|
||||
Reference in New Issue
Block a user