From 25ebf691f71d6f59cff7439e0c3ad3ffa5be3c64 Mon Sep 17 00:00:00 2001 From: Petr Date: Fri, 26 Jun 2026 20:35:23 +0200 Subject: [PATCH] security: reject file uploads exceeding 10 MB Add a MAX_UPLOAD_BYTES constant (10 MB) to Server and check the uploaded tempfile size before writing it to disk. Exceeding the limit returns HTTP 413 with a structured JSON error. Includes a test. --- lib/volumen/admin_routes.rb | 4 ++++ lib/volumen/server.rb | 1 + test/admin_test.rb | 13 +++++++++++++ 3 files changed, 18 insertions(+) diff --git a/lib/volumen/admin_routes.rb b/lib/volumen/admin_routes.rb index f849da5..5d1708e 100644 --- a/lib/volumen/admin_routes.rb +++ b/lib/volumen/admin_routes.rb @@ -88,6 +88,10 @@ module Volumen upload = params["file"] halt(400, json("error" => "no_file")) unless upload.is_a?(Hash) && upload[:tempfile] + if upload[:tempfile].size > Server::MAX_UPLOAD_BYTES + halt(413, json("error" => "file_too_large", "max_bytes" => Server::MAX_UPLOAD_BYTES)) + end + json("url" => store.store_upload(upload[:filename], upload[:tempfile])) end diff --git a/lib/volumen/server.rb b/lib/volumen/server.rb index 48db326..8d252dd 100644 --- a/lib/volumen/server.rb +++ b/lib/volumen/server.rb @@ -17,6 +17,7 @@ module Volumen class Server < Sinatra::Base MAX_LOGIN_ATTEMPTS = 10 LOGIN_WINDOW = 60 # seconds + MAX_UPLOAD_BYTES = 10 * 1024 * 1024 # 10 MB SLUG_REGEX = /\A[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?\z/ register ApiRoutes diff --git a/test/admin_test.rb b/test/admin_test.rb index 7f1531a..9fe2c2e 100644 --- a/test/admin_test.rb +++ b/test/admin_test.rb @@ -124,6 +124,19 @@ class AdminTest < Minitest::Test assert_equal 403, last_response.status end + def test_upload_rejects_file_too_large + login + get "/admin/posts/new" + token = csrf(last_response.body) + big_path = File.join(@dir, "big.png") + File.binwrite(big_path, "X" * (Volumen::Server::MAX_UPLOAD_BYTES + 1)) + post "/admin/uploads", + "_csrf" => token, + "file" => Rack::Test::UploadedFile.new(big_path, "image/png") + assert_equal 413, last_response.status + assert_equal "file_too_large", JSON.parse(last_response.body)["error"] + end + def test_image_upload_and_serving login get "/admin/posts/new"