prosody/util/format.lua
Kim Alvefur 9bbfb849b1 util.format: Restore "freeze" serialization behavior in logging
This was implied with the "debug" preset and does nice things like turn
util.set objects into "set{a,b,c}" instead of the quite verbose thing
you get otherwise.
2023-03-26 00:33:20 +01:00

153 lines
4.9 KiB
Lua

--
-- A string.format wrapper that gracefully handles invalid arguments since
-- certain format string and argument combinations may cause errors or other
-- issues like log spoofing
--
-- Provides some protection from e.g. CAPEC-135, CWE-117, CWE-134, CWE-93
local tostring = tostring;
local unpack = table.unpack;
local pack = table.pack;
local valid_utf8 = require "prosody.util.encodings".utf8.valid;
local type = type;
local dump = require"prosody.util.serialization".new({
preset = "compact";
fallback = function(v, why)
return "_[[" .. (why or tostring(v)) .. "]] ";
end;
freeze = true;
fatal = false;
maxdepth = 5;
});
local num_type = math.type;
-- In Lua 5.3+ these formats throw an error if given a float
local expects_integer = { c = true, d = true, i = true, o = true, u = true, X = true, x = true, };
-- In Lua 5.2 these throw an error given a negative number
local expects_positive = { o = true; u = true; x = true; X = true };
-- Printable Unicode replacements for control characters
local control_symbols = {
-- 0x00 .. 0x1F --> U+2400 .. U+241F, 0x7F --> U+2421
["\000"] = "\226\144\128", ["\001"] = "\226\144\129", ["\002"] = "\226\144\130",
["\003"] = "\226\144\131", ["\004"] = "\226\144\132", ["\005"] = "\226\144\133",
["\006"] = "\226\144\134", ["\007"] = "\226\144\135", ["\008"] = "\226\144\136",
["\009"] = "\226\144\137", ["\010"] = "\226\144\138", ["\011"] = "\226\144\139",
["\012"] = "\226\144\140", ["\013"] = "\226\144\141", ["\014"] = "\226\144\142",
["\015"] = "\226\144\143", ["\016"] = "\226\144\144", ["\017"] = "\226\144\145",
["\018"] = "\226\144\146", ["\019"] = "\226\144\147", ["\020"] = "\226\144\148",
["\021"] = "\226\144\149", ["\022"] = "\226\144\150", ["\023"] = "\226\144\151",
["\024"] = "\226\144\152", ["\025"] = "\226\144\153", ["\026"] = "\226\144\154",
["\027"] = "\226\144\155", ["\028"] = "\226\144\156", ["\029"] = "\226\144\157",
["\030"] = "\226\144\158", ["\031"] = "\226\144\159", ["\127"] = "\226\144\161",
};
local supports_p = pcall(string.format, "%p", ""); -- >= Lua 5.4
local function format(formatstring, ...)
local args = pack(...);
local args_length = args.n;
-- format specifier spec:
-- 1. Start: '%%'
-- 2. Flags: '[%-%+ #0]'
-- 3. Width: '%d?%d?'
-- 4. Precision: '%.?%d?%d?'
-- 5. Option: '[cdiouxXaAeEfgGqs%%]'
--
-- The options c, d, E, e, f, g, G, i, o, u, X, and x all expect a number as argument, whereas q and s expect a string.
-- This function does not accept string values containing embedded zeros, except as arguments to the q option.
-- a and A are only in Lua 5.2+
-- Lua 5.4 adds a p format that produces a pointer
-- process each format specifier
local i = 0;
formatstring = formatstring:gsub("%%[^cdiouxXaAeEfgGpqs%%]*[cdiouxXaAeEfgGpqs%%]", function(spec)
if spec == "%%" then return end
i = i + 1;
local arg = args[i];
if arg == nil then
args[i] = "nil";
return "(%s)";
end
local option = spec:sub(-1);
local t = type(arg);
if option == "s" and t == "string" and not arg:find("[%z\1-\31\128-\255]") then
-- No UTF-8 or control characters, assumed to be the common case.
return
elseif t == "number" then
if option == "g" or (option == "d" and num_type(arg) == "integer") then return end
elseif option == "s" and t ~= "string" then
arg = tostring(arg);
t = "string";
end
if option ~= "s" and option ~= "q" and option ~= "p" then
-- all other options expect numbers
if t ~= "number" then
-- arg isn't number as expected?
arg = tostring(arg);
option = "s";
spec = "[%s]";
t = "string";
elseif expects_integer[option] and num_type(arg) ~= "integer" then
args[i] = tostring(arg);
return "[%s]";
elseif expects_positive[option] and arg < 0 then
args[i] = tostring(arg);
return "[%s]";
else
return -- acceptable number
end
end
if option == "p" and not supports_p then
arg = tostring(arg);
option = "s";
spec = "[%s]";
t = "string";
end
if t == "string" and option ~= "p" then
if not valid_utf8(arg) then
option = "q";
elseif option ~= "q" then -- gets fully escaped in the next block
-- Prevent funny things with ASCII control characters and ANSI escape codes (CWE-117)
-- Also ensure embedded newlines can't look like another log line (CWE-93)
args[i] = arg:gsub("[%z\1-\8\11-\31\127]", control_symbols):gsub("\n\t?", "\n\t");
return spec;
end
end
if option == "q" then
args[i] = dump(arg);
return "%s";
end
if option == "p" and (t == "boolean" or t == "number") then
args[i] = tostring(arg);
return "[%s]";
end
end);
-- process extra args
while i < args_length do
i = i + 1;
local arg = args[i];
if arg == nil then
args[i] = "(nil)";
else
args[i] = tostring(arg):gsub("[%z\1-\8\11-\31\127]", control_symbols):gsub("\n\t?", "\n\t");
end
formatstring = formatstring .. " [%s]"
end
return formatstring:format(unpack(args));
end
return {
format = format;
};