71b99c1e18
BlobRecordSet#import_batch preserved blob keys from exported ZIP files verbatim. A crafted ZIP with a traversal key like "../../config/deploy.yml" would create a blob whose key resolves to an arbitrary filesystem path when served by ActiveStorage's DiskService, allowing an attacker to read local files. Fix: generate fresh blob keys on import, discarding whatever key was in the ZIP. This is safe because nothing else in the import pipeline references blobs by key — attachments use blob_id, ActionText uses GIDs based on record IDs, and only FileRecordSet used the old key for file data lookup. Update FileRecordSet to build an old_key→blob_id mapping from the blob JSON metadata in the ZIP, then look up blobs by ID instead of by key. Both check_record and import_batch are now fail-closed: they raise IntegrityError for unmapped storage files, missing blobs, and duplicate keys in the export. Backfill test coverage for BlobRecordSet and FileRecordSet import, and add a round-trip test verifying blob data survives export/import with regenerated keys.
46 lines
1.3 KiB
Ruby
46 lines
1.3 KiB
Ruby
require "test_helper"
|
|
|
|
class Account::DataTransfer::ActiveStorage::BlobKeyTraversalTest < ActionDispatch::IntegrationTest
|
|
test "import with path traversal blob key does not leak local files" do
|
|
blob_id = ActiveRecord::Type::Uuid.generate
|
|
traversal_key = "../../config/deploy.yml"
|
|
zip = build_zip_with_blob(id: blob_id, key: traversal_key)
|
|
Account::DataTransfer::ActiveStorage::BlobRecordSet.new(Current.account).import(from: zip)
|
|
blob = ActiveStorage::Blob.find(blob_id)
|
|
|
|
assert_not_equal traversal_key, blob.key
|
|
|
|
sign_in_as identities(:david)
|
|
get rails_blob_path(blob, disposition: "inline")
|
|
|
|
assert_response :redirect
|
|
|
|
follow_redirect!
|
|
|
|
assert_response :not_found
|
|
end
|
|
|
|
private
|
|
def build_zip_with_blob(id:, key:)
|
|
tempfile = Tempfile.new([ "malicious", ".zip" ])
|
|
tempfile.binmode
|
|
|
|
writer = ZipFile::Writer.new(tempfile)
|
|
writer.add_file("data/active_storage_blobs/#{id}.json", {
|
|
id: id,
|
|
account_id: ActiveRecord::Type::Uuid.generate,
|
|
byte_size: 32,
|
|
checksum: "",
|
|
content_type: "text/plain",
|
|
created_at: Time.current.iso8601,
|
|
filename: "traversal.txt",
|
|
key: key,
|
|
metadata: {}
|
|
}.to_json)
|
|
writer.close
|
|
|
|
tempfile.rewind
|
|
ZipFile::Reader.new(tempfile)
|
|
end
|
|
end
|