Harden history device ID validation and SD download filename
This commit is contained in:
@@ -4,3 +4,4 @@
|
|||||||
|
|
||||||
String html_escape(const String &input);
|
String html_escape(const String &input);
|
||||||
String url_encode_component(const String &input);
|
String url_encode_component(const String &input);
|
||||||
|
bool sanitize_device_id(const String &input, String &out_device_id);
|
||||||
|
|||||||
@@ -47,3 +47,52 @@ String url_encode_component(const String &input) {
|
|||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool is_hex_char(char c) {
|
||||||
|
return (c >= '0' && c <= '9') ||
|
||||||
|
(c >= 'a' && c <= 'f') ||
|
||||||
|
(c >= 'A' && c <= 'F');
|
||||||
|
}
|
||||||
|
|
||||||
|
static String to_upper_hex4(const String &input) {
|
||||||
|
String out = input;
|
||||||
|
out.toUpperCase();
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool sanitize_device_id(const String &input, String &out_device_id) {
|
||||||
|
String trimmed = input;
|
||||||
|
trimmed.trim();
|
||||||
|
if (trimmed.length() == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (trimmed.indexOf('/') >= 0 || trimmed.indexOf('\\') >= 0 || trimmed.indexOf("..") >= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (trimmed.indexOf('%') >= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed.length() == 4) {
|
||||||
|
for (size_t i = 0; i < 4; ++i) {
|
||||||
|
if (!is_hex_char(trimmed[i])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out_device_id = String("dd3-") + to_upper_hex4(trimmed);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed.length() == 8 && trimmed.startsWith("dd3-")) {
|
||||||
|
String hex = trimmed.substring(4);
|
||||||
|
for (size_t i = 0; i < 4; ++i) {
|
||||||
|
if (!is_hex_char(hex[i])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out_device_id = String("dd3-") + to_upper_hex4(hex);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
@@ -140,6 +140,42 @@ static bool checkbox_checked(const char *name) {
|
|||||||
return val == "on" || val == "true" || val == "1";
|
return val == "on" || val == "true" || val == "1";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool sanitize_history_device_id(const String &input, String &out_device_id) {
|
||||||
|
if (sanitize_device_id(input, out_device_id)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (g_statuses) {
|
||||||
|
for (uint8_t i = 0; i < g_status_count; ++i) {
|
||||||
|
String known = g_statuses[i].last_data.device_id;
|
||||||
|
if (input.equalsIgnoreCase(known) && sanitize_device_id(known, out_device_id)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String sanitize_download_filename(const String &input, bool &clean) {
|
||||||
|
String out;
|
||||||
|
out.reserve(input.length());
|
||||||
|
clean = true;
|
||||||
|
for (size_t i = 0; i < input.length(); ++i) {
|
||||||
|
unsigned char c = static_cast<unsigned char>(input[i]);
|
||||||
|
if (c < 32 || c == 127 || c == '"' || c == '\\' || c == '/') {
|
||||||
|
out += '_';
|
||||||
|
clean = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out += static_cast<char>(c);
|
||||||
|
}
|
||||||
|
out.trim();
|
||||||
|
if (out.length() == 0) {
|
||||||
|
out = "download.bin";
|
||||||
|
clean = false;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
static void history_reset() {
|
static void history_reset() {
|
||||||
if (g_history.file) {
|
if (g_history.file) {
|
||||||
g_history.file.close();
|
g_history.file.close();
|
||||||
@@ -597,7 +633,12 @@ static void handle_history_start() {
|
|||||||
server.send(200, "application/json", "{\"ok\":false,\"error\":\"time_not_synced\"}");
|
server.send(200, "application/json", "{\"ok\":false,\"error\":\"time_not_synced\"}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String device_id = server.arg("device_id");
|
String device_id_arg = server.arg("device_id");
|
||||||
|
String device_id;
|
||||||
|
if (!sanitize_history_device_id(device_id_arg, device_id)) {
|
||||||
|
server.send(200, "application/json", "{\"ok\":false,\"error\":\"bad_device_id\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
uint16_t days = static_cast<uint16_t>(server.arg("days").toInt());
|
uint16_t days = static_cast<uint16_t>(server.arg("days").toInt());
|
||||||
uint16_t res_min = static_cast<uint16_t>(server.arg("res").toInt());
|
uint16_t res_min = static_cast<uint16_t>(server.arg("res").toInt());
|
||||||
String mode_str = server.arg("mode");
|
String mode_str = server.arg("mode");
|
||||||
@@ -652,7 +693,12 @@ static void handle_history_data() {
|
|||||||
if (!ensure_auth()) {
|
if (!ensure_auth()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String device_id = server.arg("device_id");
|
String device_id_arg = server.arg("device_id");
|
||||||
|
String device_id;
|
||||||
|
if (!sanitize_history_device_id(device_id_arg, device_id)) {
|
||||||
|
server.send(200, "application/json", "{\"ready\":false,\"error\":\"bad_device_id\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!g_history.bins || device_id.length() == 0 || device_id != g_history.device_id) {
|
if (!g_history.bins || device_id.length() == 0 || device_id != g_history.device_id) {
|
||||||
server.send(200, "application/json", "{\"ready\":false,\"error\":\"no_job\"}");
|
server.send(200, "application/json", "{\"ready\":false,\"error\":\"no_job\"}");
|
||||||
return;
|
return;
|
||||||
@@ -716,7 +762,11 @@ static void handle_sd_download() {
|
|||||||
}
|
}
|
||||||
size_t size = f.size();
|
size_t size = f.size();
|
||||||
String filename = path.substring(path.lastIndexOf('/') + 1);
|
String filename = path.substring(path.lastIndexOf('/') + 1);
|
||||||
server.sendHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
|
bool name_clean = true;
|
||||||
|
(void)name_clean;
|
||||||
|
String safe_name = sanitize_download_filename(filename, name_clean);
|
||||||
|
String cd = "attachment; filename=\"" + safe_name + "\"; filename*=UTF-8''" + url_encode_component(safe_name);
|
||||||
|
server.sendHeader("Content-Disposition", cd);
|
||||||
server.setContentLength(size);
|
server.setContentLength(size);
|
||||||
const char *content_type = "application/octet-stream";
|
const char *content_type = "application/octet-stream";
|
||||||
if (filename.endsWith(".csv")) {
|
if (filename.endsWith(".csv")) {
|
||||||
|
|||||||
@@ -12,9 +12,25 @@ static void test_html_escape_basic() {
|
|||||||
TEST_ASSERT_EQUAL_STRING("&<>"'", html_escape("&<>\"'").c_str());
|
TEST_ASSERT_EQUAL_STRING("&<>"'", html_escape("&<>\"'").c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void test_sanitize_device_id() {
|
||||||
|
String out;
|
||||||
|
TEST_ASSERT_TRUE(sanitize_device_id("F19C", out));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("dd3-F19C", out.c_str());
|
||||||
|
TEST_ASSERT_TRUE(sanitize_device_id("dd3-f19c", out));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("dd3-F19C", out.c_str());
|
||||||
|
TEST_ASSERT_FALSE(sanitize_device_id("F19G", out));
|
||||||
|
TEST_ASSERT_FALSE(sanitize_device_id("dd3-12", out));
|
||||||
|
TEST_ASSERT_FALSE(sanitize_device_id("dd3-12345", out));
|
||||||
|
TEST_ASSERT_FALSE(sanitize_device_id("../F19C", out));
|
||||||
|
TEST_ASSERT_FALSE(sanitize_device_id("dd3-%2f", out));
|
||||||
|
TEST_ASSERT_FALSE(sanitize_device_id("dd3-12/3", out));
|
||||||
|
TEST_ASSERT_FALSE(sanitize_device_id("dd3-12\\3", out));
|
||||||
|
}
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
UNITY_BEGIN();
|
UNITY_BEGIN();
|
||||||
RUN_TEST(test_html_escape_basic);
|
RUN_TEST(test_html_escape_basic);
|
||||||
|
RUN_TEST(test_sanitize_device_id);
|
||||||
UNITY_END();
|
UNITY_END();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user