security: whitelist allowed image MIME types for uploads
Only accept common image formats (JPEG, PNG, GIF, WebP, AVIF, SVG) on the upload endpoint. Non-image uploads now return HTTP 415 with a structured JSON error listing the allowed types. Includes a test.
This commit is contained in:
@@ -92,6 +92,12 @@ module Volumen
|
|||||||
halt(413, json("error" => "file_too_large", "max_bytes" => Server::MAX_UPLOAD_BYTES))
|
halt(413, json("error" => "file_too_large", "max_bytes" => Server::MAX_UPLOAD_BYTES))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
content_type_str = upload[:type].to_s.split(";").first.to_s.strip.downcase
|
||||||
|
unless Server::ALLOWED_UPLOAD_TYPES.include?(content_type_str)
|
||||||
|
halt(415, json("error" => "unsupported_media_type",
|
||||||
|
"allowed" => Server::ALLOWED_UPLOAD_TYPES))
|
||||||
|
end
|
||||||
|
|
||||||
json("url" => store.store_upload(upload[:filename], upload[:tempfile]))
|
json("url" => store.store_upload(upload[:filename], upload[:tempfile]))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ module Volumen
|
|||||||
MAX_LOGIN_ATTEMPTS = 10
|
MAX_LOGIN_ATTEMPTS = 10
|
||||||
LOGIN_WINDOW = 60 # seconds
|
LOGIN_WINDOW = 60 # seconds
|
||||||
MAX_UPLOAD_BYTES = 10 * 1024 * 1024 # 10 MB
|
MAX_UPLOAD_BYTES = 10 * 1024 * 1024 # 10 MB
|
||||||
|
ALLOWED_UPLOAD_TYPES = %w[
|
||||||
|
image/jpeg image/png image/gif image/webp image/avif image/svg+xml
|
||||||
|
].freeze
|
||||||
SLUG_REGEX = /\A[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?\z/
|
SLUG_REGEX = /\A[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?\z/
|
||||||
|
|
||||||
register ApiRoutes
|
register ApiRoutes
|
||||||
|
|||||||
@@ -124,6 +124,19 @@ class AdminTest < Minitest::Test
|
|||||||
assert_equal 403, last_response.status
|
assert_equal 403, last_response.status
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_upload_rejects_unsupported_media_type
|
||||||
|
login
|
||||||
|
get "/admin/posts/new"
|
||||||
|
token = csrf(last_response.body)
|
||||||
|
txt_path = File.join(@dir, "readme.txt")
|
||||||
|
File.binwrite(txt_path, "hello")
|
||||||
|
post "/admin/uploads",
|
||||||
|
"_csrf" => token,
|
||||||
|
"file" => Rack::Test::UploadedFile.new(txt_path, "text/plain")
|
||||||
|
assert_equal 415, last_response.status
|
||||||
|
assert_equal "unsupported_media_type", JSON.parse(last_response.body)["error"]
|
||||||
|
end
|
||||||
|
|
||||||
def test_upload_rejects_file_too_large
|
def test_upload_rejects_file_too_large
|
||||||
login
|
login
|
||||||
get "/admin/posts/new"
|
get "/admin/posts/new"
|
||||||
|
|||||||
Reference in New Issue
Block a user