INIT_LOG_MODULE(archive)
+static const unsigned int max_archive_files = 100000;
+
static GQuark
rspamd_archives_err_quark(void)
{
return q;
}
+static void
+rspamd_archive_file_free(struct rspamd_archive_file *f)
+{
+ if (f->fname) {
+ g_string_free(f->fname, TRUE);
+ }
+
+ g_free(f);
+}
+
static void
rspamd_archive_dtor(gpointer p)
{
for (i = 0; i < arch->files->len; i++) {
f = g_ptr_array_index(arch->files, i);
+ rspamd_archive_file_free(f);
+ }
- if (f->fname) {
- g_string_free(f->fname, TRUE);
+ g_ptr_array_free(arch->files, TRUE);
+}
+
+static gboolean
+rspamd_archive_file_limit_check(struct rspamd_task *task,
+ struct rspamd_archive *arch)
+{
+ if (MESSAGE_FIELD(task, narchive_files) >= max_archive_files) {
+ arch->flags |= RSPAMD_ARCHIVE_FILES_TRUNCATED;
+
+ if (!MESSAGE_FIELD(task, archive_files_limit_reached)) {
+ msg_warn_task("archive file metadata limit of %ud is reached; "
+ "ignoring the remaining entries",
+ max_archive_files);
+ MESSAGE_FIELD(task, archive_files_limit_reached) = TRUE;
}
- g_free(f);
+ return FALSE;
}
- g_ptr_array_free(arch->files, TRUE);
+ return TRUE;
+}
+
+static gboolean
+rspamd_archive_file_add(struct rspamd_task *task,
+ struct rspamd_archive *arch,
+ struct rspamd_archive_file *f)
+{
+ if (!rspamd_archive_file_limit_check(task, arch)) {
+ rspamd_archive_file_free(f);
+
+ return FALSE;
+ }
+
+ MESSAGE_FIELD(task, narchive_files) += 1;
+ g_ptr_array_add(arch->files, f);
+
+ return TRUE;
+}
+
+static void
+rspamd_archive_files_reset(struct rspamd_task *task,
+ struct rspamd_archive *arch)
+{
+ g_assert(MESSAGE_FIELD(task, narchive_files) >= arch->files->len);
+ MESSAGE_FIELD(task, narchive_files) -= arch->files->len;
+ rspamd_archive_dtor(arch);
+ arch->files = g_ptr_array_new();
+ arch->flags &= ~RSPAMD_ARCHIVE_FILES_TRUNCATED;
}
}
rspamd_mempool_add_destructor(task->task_pool, rspamd_archive_dtor,
arch);
+ if (!rspamd_archive_file_limit_check(task, arch)) {
+ goto set;
+ }
while (cd < start + cd_offset + cd_size) {
uint16_t flags;
arch->flags |= RSPAMD_ARCHIVE_HAS_OBFUSCATED_FILES;
}
- g_ptr_array_add(arch->files, f);
+ if (!rspamd_archive_file_add(task, arch, f)) {
+ break;
+ }
msg_debug_archive("found file in zip archive: %v", f->fname);
}
else {
cd += fname_len + comment_len + extra_len + cd_basic_len;
}
+set:
part->part_type = RSPAMD_MIME_PART_ARCHIVE;
part->specific.arch = arch;
}
rspamd_mempool_add_destructor(task->task_pool, rspamd_archive_dtor,
arch);
+ if (!rspamd_archive_file_limit_check(task, arch)) {
+ goto end;
+ }
while (p < end) {
/* Crc16 */
if (f->flags & RSPAMD_ARCHIVE_FILE_OBFUSCATED) {
arch->flags |= RSPAMD_ARCHIVE_HAS_OBFUSCATED_FILES;
}
- g_ptr_array_add(arch->files, f);
+ if (!rspamd_archive_file_add(task, arch, f)) {
+ goto end;
+ }
}
else {
g_free(f);
}
rspamd_mempool_add_destructor(task->task_pool, rspamd_archive_dtor,
arch);
+ if (!rspamd_archive_file_limit_check(task, arch)) {
+ goto end;
+ }
/* Now we can have either encryption header or archive header */
/* Crc 32 */
if (f->fname) {
msg_debug_archive("added rarv5 file: %v", f->fname);
- g_ptr_array_add(arch->files, f);
+ if (!rspamd_archive_file_add(task, arch, f)) {
+ f = NULL;
+ goto end;
+ }
if (f->flags & RSPAMD_ARCHIVE_FILE_OBFUSCATED) {
arch->flags |= RSPAMD_ARCHIVE_HAS_OBFUSCATED_FILES;
}
if (res != NULL) {
fentry = g_malloc0(sizeof(*fentry));
fentry->fname = res;
- g_ptr_array_add(arch->files, fentry);
+ if (!rspamd_archive_file_add(task, arch, fentry)) {
+ p = NULL;
+ goto end;
+ }
msg_debug_archive("7zip: found file %v", res);
}
else {
}
/* Clean the existing files if any */
- rspamd_archive_dtor(arch);
- arch->files = g_ptr_array_new();
+ rspamd_archive_files_reset(task, arch);
struct archive_entry *ae;
msg_debug_archive("7zip: found file %s", name);
struct rspamd_archive_file *f = g_malloc0(sizeof(*f));
f->fname = g_string_new(name);
- g_ptr_array_add(arch->files, f);
+ if (!rspamd_archive_file_add(task, arch, f)) {
+ break;
+ }
}
archive_read_data_skip(a);
}
arch->type = RSPAMD_ARCHIVE_7ZIP;
rspamd_mempool_add_destructor(task->task_pool, rspamd_archive_dtor,
arch);
+ if (!rspamd_archive_file_limit_check(task, arch)) {
+ goto end;
+ }
/* Magic (6 bytes) + version (2 bytes) + crc32 (4 bytes) */
p += sizeof(uint64_t) + sizeof(uint32_t);
while ((p = rspamd_7zip_read_next_section(task, p, end, arch, part)) != NULL);
+end:
part->part_type = RSPAMD_MIME_PART_ARCHIVE;
part->specific.arch = arch;
if (part->cd != NULL) {
}
rspamd_mempool_add_destructor(task->task_pool, rspamd_archive_dtor,
arch);
+ if (!rspamd_archive_file_limit_check(task, arch)) {
+ goto set;
+ }
flags = p[3];
fname_start, p - fname_start);
if (f->fname) {
- g_ptr_array_add(arch->files, f);
+ if (!rspamd_archive_file_add(task, arch, f)) {
+ goto set;
+ }
if (f->flags & RSPAMD_ARCHIVE_FILE_OBFUSCATED) {
arch->flags |= RSPAMD_ARCHIVE_HAS_OBFUSCATED_FILES;
msg_debug_archive("fallback to gzip filename based on cd: %v",
f->fname);
- g_ptr_array_add(arch->files, f);
+ if (!rspamd_archive_file_add(task, arch, f)) {
+ goto set;
+ }
goto set;
}
msg_debug_archive("fallback to gzip filename based on cd: %v",
f->fname);
- g_ptr_array_add(arch->files, f);
+ if (!rspamd_archive_file_add(task, arch, f)) {
+ goto set;
+ }
goto set;
}
* * `get_files` - return list of strings with filenames inside archive
* * `get_files_full` - return list of tables with all information about files
* * `is_encrypted` - return true if an archive is encrypted
+ * * `is_truncated` - return true if the archive file list hit the task limit
* * `get_type` - return string representation of image's type (e.g. 'zip')
* * `get_filename` - return string with archive's file name
* * `get_size` - return size in bytes
LUA_FUNCTION_DEF(archive, is_encrypted);
LUA_FUNCTION_DEF(archive, is_obfuscated);
LUA_FUNCTION_DEF(archive, is_unreadable);
+LUA_FUNCTION_DEF(archive, is_truncated);
LUA_FUNCTION_DEF(archive, get_filename);
LUA_FUNCTION_DEF(archive, get_size);
LUA_INTERFACE_DEF(archive, is_encrypted),
LUA_INTERFACE_DEF(archive, is_obfuscated),
LUA_INTERFACE_DEF(archive, is_unreadable),
+ LUA_INTERFACE_DEF(archive, is_truncated),
LUA_INTERFACE_DEF(archive, get_filename),
LUA_INTERFACE_DEF(archive, get_size),
{"__tostring", rspamd_lua_class_tostring},
return 1;
}
+static int
+lua_archive_is_truncated(lua_State *L)
+{
+ LUA_TRACE_POINT;
+ struct rspamd_archive *arch = lua_check_archive(L);
+
+ if (arch != NULL) {
+ lua_pushboolean(L, (arch->flags & RSPAMD_ARCHIVE_FILES_TRUNCATED) ? true : false);
+ }
+ else {
+ return luaL_error(L, "invalid arguments");
+ }
+
+ return 1;
+}
+
static int
lua_archive_get_size(lua_State *L)
{
To: <nobody@example.com>
Subject: test
]]
+ local function make_many_file_zip(nfiles)
+ local rspamd_util = require("rspamd_util")
+ local local_header = "PK\3\4" .. string.rep("\0", 26)
+ local cd_record = "PK\1\2" .. string.rep("\0", 24) ..
+ "\1\0" .. string.rep("\0", 16) .. "x"
+ local cd = string.rep(cd_record, nfiles)
+ local eocd = "PK\5\6" .. string.rep("\0", 8) ..
+ rspamd_util.pack("<I4", #cd) ..
+ rspamd_util.pack("<I4", #local_header) .. "\0\0"
+
+ return local_header .. cd .. eocd
+ end
local mpart = [[
Content-Type: multipart/mixed; boundary=XXX
]]
task:destroy()
end)
+ test("Archive file metadata count is bounded", function()
+ local msg = table.concat {
+ hdrs,
+ "Content-Type: application/zip\n",
+ "Content-Disposition: attachment; filename=many.zip\n",
+ "Content-Transfer-Encoding: binary\n",
+ "\n",
+ make_many_file_zip(100001),
+ }
+ local res, task = rspamd_task.load_from_string(msg, rspamd_config)
+ assert_true(res, "failed to load message")
+ task:process_message()
+
+ local archives = task:get_archives()
+ assert_equal(1, #archives, "archive was not detected")
+ local files = archives[1]:get_files()
+ assert_equal(100000, #files,
+ string.format("unexpected archive file count: %d", #files))
+ assert_true(archives[1]:is_truncated(),
+ "archive metadata truncation was not exposed")
+ task:destroy()
+ end)
+
+ test("Archive file metadata count is bounded across parts", function()
+ local zip = make_many_file_zip(1000)
+ local body = {}
+
+ for i = 1, 101 do
+ body[#body + 1] = table.concat {
+ "--MANY-ARCHIVES\n",
+ "Content-Type: application/zip\n",
+ string.format(
+ "Content-Disposition: attachment; filename=archive-%d.zip\n", i),
+ "Content-Transfer-Encoding: binary\n",
+ "\n",
+ zip,
+ "\n",
+ }
+ end
+ body[#body + 1] = "--MANY-ARCHIVES--\n"
+
+ local msg = table.concat {
+ hdrs,
+ "Content-Type: multipart/mixed; boundary=MANY-ARCHIVES\n",
+ "\n",
+ table.concat(body),
+ }
+ local res, task = rspamd_task.load_from_string(msg, rspamd_config)
+ assert_true(res, "failed to load message")
+ task:process_message()
+
+ local archives = task:get_archives()
+ assert_equal(101, #archives, "archives were not detected")
+ local nfiles = 0
+ for _, archive in ipairs(archives) do
+ nfiles = nfiles + #archive:get_files()
+ end
+ assert_equal(100000, nfiles,
+ string.format("unexpected archive file count across parts: %d", nfiles))
+ assert_false(archives[100]:is_truncated(),
+ "archive within the task budget was marked truncated")
+ assert_true(archives[101]:is_truncated(),
+ "archive exceeding the task budget was not marked truncated")
+ task:destroy()
+ end)
+
test("Part URLs are not deduplicated across MIME parts", function()
local msg = table.concat {
hdrs,