Sanitize formspec fields server-side (#14878)

This commit is contained in:
sfan5 2024-08-21 21:34:46 +02:00 committed by GitHub
parent ab7af5d15a
commit c6ef5ab259
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 99 additions and 5 deletions

@ -2625,6 +2625,9 @@ background elements are drawn before all other elements.
**WARNING**: do _not_ use an element name starting with `key_`; those names are **WARNING**: do _not_ use an element name starting with `key_`; those names are
reserved to pass key press events to formspec! reserved to pass key press events to formspec!
**WARNING**: names and values of elements cannot contain binary data such as ASCII
control characters. For values, escape sequences used by the engine are an exception to this.
**WARNING**: Minetest allows you to add elements to every single formspec instance **WARNING**: Minetest allows you to add elements to every single formspec instance
using `player:set_formspec_prepend()`, which may be the reason backgrounds are using `player:set_formspec_prepend()`, which may be the reason backgrounds are
appearing when you don't expect them to, or why things are styled differently appearing when you don't expect them to, or why things are styled differently

@ -1351,15 +1351,22 @@ static bool pkt_read_formspec_fields(NetworkPacket *pkt, StringMap &fields)
u16 field_count; u16 field_count;
*pkt >> field_count; *pkt >> field_count;
u64 length = 0; size_t length = 0;
for (u16 k = 0; k < field_count; k++) { for (u16 k = 0; k < field_count; k++) {
std::string fieldname; std::string fieldname, fieldvalue;
*pkt >> fieldname; *pkt >> fieldname;
fields[fieldname] = pkt->readLongString(); fieldvalue = pkt->readLongString();
length += fieldname.size(); fieldname = sanitize_untrusted(fieldname, false);
length += fields[fieldname].size(); // We'd love to strip escapes here but some formspec elements reflect data
// from the server (e.g. dropdown), which can contain translations.
fieldvalue = sanitize_untrusted(fieldvalue);
length += fieldname.size() + fieldvalue.size();
fields[std::move(fieldname)] = std::move(fieldvalue);
} }
// 640K ought to be enough for anyone // 640K ought to be enough for anyone
return length < 640 * 1024; return length < 640 * 1024;
} }

@ -61,6 +61,7 @@ public:
void testSanitizeDirName(); void testSanitizeDirName();
void testIsBlockInSight(); void testIsBlockInSight();
void testColorizeURL(); void testColorizeURL();
void testSanitizeUntrusted();
}; };
static TestUtilities g_test_instance; static TestUtilities g_test_instance;
@ -95,6 +96,7 @@ void TestUtilities::runTests(IGameDef *gamedef)
TEST(testSanitizeDirName); TEST(testSanitizeDirName);
TEST(testIsBlockInSight); TEST(testIsBlockInSight);
TEST(testColorizeURL); TEST(testColorizeURL);
TEST(testSanitizeUntrusted);
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
@ -743,3 +745,28 @@ void TestUtilities::testColorizeURL()
warningstream << "Test skipped." << std::endl; warningstream << "Test skipped." << std::endl;
#endif #endif
} }
void TestUtilities::testSanitizeUntrusted()
{
std::string_view t1{u8"Anästhesieausrüstung"};
UASSERTEQ(auto, sanitize_untrusted(t1), t1);
std::string_view t2{"stop\x00here", 9};
UASSERTEQ(auto, sanitize_untrusted(t2), "stop");
UASSERTEQ(auto, sanitize_untrusted("\x01\x08\x13\x1dhello\r\n\tworld"), "hello\n\tworld");
std::string_view t3{"some \x1b(T@whatever)text\x1b" "E here"};
UASSERTEQ(auto, sanitize_untrusted(t3), t3);
auto t3_sanitized = sanitize_untrusted(t3, false);
UASSERT(str_starts_with(t3_sanitized, "some ") && str_ends_with(t3_sanitized, " here"));
UASSERT(t3_sanitized.find('\x1b') == std::string::npos);
UASSERTEQ(auto, sanitize_untrusted("\x1b[31m"), "[31m");
// edge cases
for (bool keep : {true, false}) {
UASSERTEQ(auto, sanitize_untrusted("\x1b", keep), "");
UASSERTEQ(auto, sanitize_untrusted("\x1b(", keep), "(");
}
}

@ -950,6 +950,53 @@ std::string sanitizeDirName(std::string_view str, std::string_view optional_pref
return wide_to_utf8(safe_name); return wide_to_utf8(safe_name);
} }
template <class F>
void remove_indexed(std::string &s, F pred)
{
size_t j = 0;
for (size_t i = 0; i < s.length();) {
if (pred(s, i++))
j++;
if (i != j)
s[j] = s[i];
}
s.resize(j);
}
std::string sanitize_untrusted(std::string_view str, bool keep_escapes)
{
// truncate on NULL
std::string s{str.substr(0, str.find('\0'))};
// remove control characters except tab, feed and escape
s.erase(std::remove_if(s.begin(), s.end(), [] (unsigned char c) {
return c < 9 || (c >= 13 && c < 27) || (c >= 28 && c < 32);
}), s.end());
if (!keep_escapes) {
s.erase(std::remove(s.begin(), s.end(), '\x1b'), s.end());
return s;
}
// Note: Minetest escapes generally just look like \x1b# or \x1b(###)
// where # is a single character and ### any number of characters.
// Here we additionally assume that the first character in the sequence
// is [A-Za-z], to enable us to filter foreign types of escapes that might
// be unsafe e.g. ANSI escapes in a terminal.
const auto &check = [] (char c) {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
};
remove_indexed(s, [&check] (const std::string &s, size_t i) {
if (s[i] != '\x1b')
return true;
if (i+1 >= s.length())
return false;
if (s[i+1] == '(')
return i+2 < s.length() && check(s[i+2]); // long-form escape
else
return check(s[i+1]); // short-form escape
});
return s;
}
void safe_print_string(std::ostream &os, std::string_view str) void safe_print_string(std::ostream &os, std::string_view str)
{ {

@ -761,6 +761,16 @@ inline irr::core::stringw utf8_to_stringw(std::string_view input)
*/ */
std::string sanitizeDirName(std::string_view str, std::string_view optional_prefix); std::string sanitizeDirName(std::string_view str, std::string_view optional_prefix);
/**
* Sanitize an untrusted string (e.g. from the network). This will get strip
* control characters and (optionally) any MT-style escape sequences too.
* Note that they won't be removed cleanly but rather just broken, unlike with
* unescape_enriched.
* Line breaks and UTF-8 is permitted.
*/
[[nodiscard]]
std::string sanitize_untrusted(std::string_view str, bool keep_escapes = true);
/** /**
* Prints a sanitized version of a string without control characters. * Prints a sanitized version of a string without control characters.
* '\t' and '\n' are allowed, as are UTF-8 control characters (e.g. RTL). * '\t' and '\n' are allowed, as are UTF-8 control characters (e.g. RTL).