diff --git a/lib/resty/ldap/filter.lua b/lib/resty/ldap/filter.lua index 7bb67f9..b064cea 100644 --- a/lib/resty/ldap/filter.lua +++ b/lib/resty/ldap/filter.lua @@ -199,21 +199,26 @@ local ESCAPE_MAP = { ["="] = "\\3d", ["<"] = "\\3c", [">"] = "\\3e", + ["~"] = "\\7e", } -- RFC 4515 s3: escape the octets that are special in an assertion value so that -- untrusted input cannot change the filter's structure. The compiler un-escapes -- \HH back to raw bytes, so escape() is its exact inverse for these octets. --- The five RFC-mandated specials are `* ( ) \ NUL`; `= < >` are not RFC-special +-- The five RFC-mandated specials are `* ( ) \ NUL`; `= < > ~` are not RFC-special -- but the grammar above is stricter and rejects them raw inside a value, so we --- escape them too (escaping any octet is RFC-legal and round-trips). Note: valid --- multi-byte UTF-8 passes through the grammar unescaped; a lone invalid-UTF-8 --- byte (0x80-0xFF) is not escaped here and fails the filter closed. +-- escape them too (escaping any octet is RFC-legal and round-trips). `~` is the +-- subtler one: UTF8_FILTERED_CHARACTER excludes it so that `~=` can be read as +-- the approx operator, and the Cmt that hands a raw `~` back to the value only +-- fires away from the end of the filter string -- so a leading or trailing `~` +-- would otherwise fail a value RFC 4515 permits (UTF1SUBSET is %x5D-7F). +-- Note: valid multi-byte UTF-8 passes through the grammar unescaped; a lone +-- invalid-UTF-8 byte (0x80-0xFF) is not escaped here and fails the filter closed. function _M.escape(value) if type(value) ~= "string" then return nil, "value must be a string" end - return (value:gsub("[%z%*%(%)\\=<>]", ESCAPE_MAP)) + return (value:gsub("[%z%*%(%)\\=<>~]", ESCAPE_MAP)) end diff --git a/t/filter.t b/t/filter.t index 5626762..b197fef 100644 --- a/t/filter.t +++ b/t/filter.t @@ -519,3 +519,35 @@ GET /t ok --- no_error_log [error] + +=== TEST 8: escape covers ~, which RFC 4515 allows but the grammar rejects at the edges +--- http_config eval: $::HttpConfig +--- config + location /t { + content_by_lua_block { + local filter = require("resty.ldap.filter") + assert(filter.escape("a~b") == "a\\7eb", "tilde") + -- RFC 4515 UTF1SUBSET (%x5D-7F) includes ~ (0x7e), so these are all + -- legal assertion values. The grammar only tolerates a raw ~ mid-value + -- and only away from the end of the filter string, so escaping is what + -- makes the leading and trailing forms usable. + for _, raw in ipairs({ "admin~", "~admin", "ad~min", "admin~~", "~" }) do + local ast = assert(filter.compile("(cn=" .. filter.escape(raw) .. ")"), + "value must compile after escaping: " .. raw) + assert(ast.item_type == "simple" and ast.filter_type == "equal", + "simple equality: " .. raw) + assert(ast.attribute_value == raw, + "round-trips to raw: " .. tostring(ast.attribute_value)) + end + -- ~= stays an operator: escaping only ever applies to the value + local approx = assert(filter.compile("(cn~=admin)"), "approx filter still parses") + assert(approx.filter_type == "approx", "filter_type != approx, " .. approx.filter_type) + ngx.say("ok") + } + } +--- request +GET /t +--- response_body +ok +--- no_error_log +[error]