security: CORS preflight, session invalidation, CSRF on preview, symlink containment, and hardening

- Add OPTIONS /api/volumen/* for CORS preflight (S-22).
- Invalidate session in require_login! when user no longer exists (S-8).
- Add check_csrf! to POST /admin/preview (S-1).
- Use File.realpath for symlink-safe containment in media_file (S-2).
- Memoize Users#all with mtime-based invalidation (S-12).
- Document that empty permitted_hosts means allow-all (S-5).
- Warn and fall back to random when session_key is too short (S-20).
This commit is contained in:
2026-06-25 22:06:49 +02:00
parent 5b8236c0a4
commit 52be2bb996
8 changed files with 76 additions and 9 deletions
+19
View File
@@ -263,6 +263,25 @@ class AdminTest < Minitest::Test
assert_includes last_response.body, "Username unchanged"
end
def test_preview_requires_csrf
login
get "/admin/posts/new"
post "/admin/preview", "body" => "**hi**"
assert_equal 403, last_response.status
end
def test_orphaned_session_is_rejected
login
session_for = -> { rack_mock_session.cookie_jar["rack.session"].to_s }
refute_empty session_for.call, "expected a session cookie after login"
# Simulate a deleted user by clearing users.toml while keeping the cookie.
File.write(@users_path, "")
get "/admin/"
assert_equal 302, last_response.status
assert_equal "http://example.org/admin/login", last_response.location
end
private
def csrf(body)
+7
View File
@@ -119,4 +119,11 @@ class ServerTest < Minitest::Test
assert_includes body, "<loc>https://example.com/hello</loc>"
refute_includes body, "draft"
end
def test_cors_preflight
options "/api/volumen/site"
assert_predicate last_response, :ok?
assert_equal "*", last_response.headers["Access-Control-Allow-Origin"]
assert_equal "GET, OPTIONS", last_response.headers["Access-Control-Allow-Methods"]
end
end