From 58507103de6cddf9875cba938805f95deb2be314 Mon Sep 17 00:00:00 2001 From: Derek Argueta Date: Fri, 21 Aug 2026 21:39:29 -0500 Subject: [PATCH] lua: add base64Decode() to the stream handle Commit Message: lua: add base64Decode() to the stream handle Additional Description: The HTTP Lua filter has offered base64Escape() since #21764 but never a decode counterpart, so a script receiving a base64 value -- a header carrying an encoded claim, an upstream body field that protojson encoded as bytes -- had to either hand-roll a decoder in Lua or reach for a C module that is not present in every Envoy build. base64Decode() returns nil on malformed input rather than raising, so a value that arrived over the wire can be checked instead of trusted. Raising would be awkward here: the common sources are all attacker-influenced, and a Lua error means the filter is skipped entirely, which for an admission-style script turns a deny into an allow. Risk Level: low Testing: unit test covering a known vector, a round trip through base64Escape(), embedded NUL bytes, the empty string, and both invalid-length and invalid-alphabet inputs. Docs Changes: added a base64Decode() entry to the Lua filter docs. Release Notes: added. Platform Specific Features: N/A Fixes #46873 Signed-off-by: Derek Argueta --- .../new_features/lua__added-base64-decode.rst | 4 ++ .../http/http_filters/lua_filter.rst | 31 +++++++++++++ .../extensions/filters/http/lua/lua_filter.cc | 14 ++++++ .../extensions/filters/http/lua/lua_filter.h | 8 ++++ .../filters/http/lua/lua_filter_test.cc | 45 +++++++++++++++++++ 5 files changed, 102 insertions(+) create mode 100644 changelogs/current/new_features/lua__added-base64-decode.rst diff --git a/changelogs/current/new_features/lua__added-base64-decode.rst b/changelogs/current/new_features/lua__added-base64-decode.rst new file mode 100644 index 000000000000..2088fbab4ddd --- /dev/null +++ b/changelogs/current/new_features/lua__added-base64-decode.rst @@ -0,0 +1,4 @@ +Added the :ref:`base64Decode() +` method to the HTTP Lua filter's stream +handle, the inverse of the existing ``base64Escape()``. It returns ``nil`` when the input is not +valid base64. diff --git a/docs/root/configuration/http/http_filters/lua_filter.rst b/docs/root/configuration/http/http_filters/lua_filter.rst index 478890854069..943f19c0540a 100644 --- a/docs/root/configuration/http/http_filters/lua_filter.rst +++ b/docs/root/configuration/http/http_filters/lua_filter.rst @@ -667,6 +667,37 @@ which means the signature is verified; otherwise, the second element will store Encodes the input string as base64. This can be useful for escaping binary data. +.. _config_http_filters_lua_stream_handle_api_base64_decode: + +``base64Decode()`` +^^^^^^^^^^^^^^^^^^ + +.. code-block:: lua + + local decoded = handle:base64Decode("aW5wdXQgc3RyaW5n") + +Decodes a base64 encoded string, the inverse of :ref:`base64Escape() +`. Returns ``nil`` if the input is not +valid base64, so a value taken from a header or an upstream body can be checked rather than +having to be trusted: + +.. code-block:: lua + + function envoy_on_request(request_handle) + local claim = request_handle:headers():get("x-encoded-claim") + if claim ~= nil then + local decoded = request_handle:base64Decode(claim) + if decoded == nil then + request_handle:respond({[":status"] = "400"}, "malformed claim") + return + end + request_handle:headers():add("x-decoded-claim", decoded) + end + end + +The decoded value may contain NUL bytes, since base64 carries arbitrary binary data; Lua strings +are length-counted, so this is preserved. + ``timestamp()`` ^^^^^^^^^^^^^^^ diff --git a/source/extensions/filters/http/lua/lua_filter.cc b/source/extensions/filters/http/lua/lua_filter.cc index e3e2d73ebee6..72ea2dea5c48 100644 --- a/source/extensions/filters/http/lua/lua_filter.cc +++ b/source/extensions/filters/http/lua/lua_filter.cc @@ -810,6 +810,20 @@ int StreamHandleWrapper::luaBase64Escape(lua_State* state) { return 1; } +int StreamHandleWrapper::luaBase64Decode(lua_State* state) { + absl::string_view input = Filters::Common::Lua::getStringViewFromLuaString(state, 2); + std::string output; + if (!absl::Base64Unescape(input, &output)) { + // Returning nil rather than raising keeps a malformed value recoverable by the script, which + // is the common case when the input came from a header or an upstream response body. + lua_pushnil(state); + return 1; + } + lua_pushlstring(state, output.data(), output.size()); + + return 1; +} + int StreamHandleWrapper::luaTimestamp(lua_State* state) { auto now = time_source_.systemTime().time_since_epoch(); diff --git a/source/extensions/filters/http/lua/lua_filter.h b/source/extensions/filters/http/lua/lua_filter.h index 4f2dd7651177..2ad6304d2667 100644 --- a/source/extensions/filters/http/lua/lua_filter.h +++ b/source/extensions/filters/http/lua/lua_filter.h @@ -224,6 +224,7 @@ class StreamHandleWrapper : public Filters::Common::Lua::BaseLuaObjectdecodeHeaders(request_headers, true))); + + Http::TestResponseHeaderMapImpl response_headers{{":status", "200"}}; + EXPECT_LOG_CONTAINS_ALL_OF( + Envoy::ExpectedLogMessages({{"trace", "bad chars: nil"}, {"trace", "bad length: nil"}}), + EXPECT_EQ(Http::FilterHeadersStatus::Continue, + filter_->encodeHeaders(response_headers, true))); +} + TEST_F(LuaHttpFilterTest, Timestamp_ReturnsFormatSet) { const std::string SCRIPT{R"EOF( function envoy_on_request(request_handle)