Root cause
Other / insufficient public mechanism detail introduced
The AI-assisted change below is where the vulnerable behavior appears.
How AI contributed: Direct introductionPublished Apr 10, 2026 in
bugsink/bugsink
Provisional finding. Full causal and release verification remains incomplete.
An AI-assisted streaming change used a caller-supplied whole-file checksum as a temporary output path and wrote file content before checking the digest. A valid global upload token and referenced chunks are required. A later checksum failure does not undo the filesystem write, so upload authority can extend to creating or overwriting files writable by the service account.
Root cause
The AI-assisted change below is where the vulnerable behavior appears.
Fix
The repair uses server-selected temporary files instead of caller-derived destinations. Source tags establish 2.1.0 as affected and 2.1.1 as repaired; unauthenticated access, direct code execution, and built wheel or container equivalence are not established.
Case facts
@@ -121,23 +197,16 @@+ max_file_size = get_settings().MAX_FILE_SIZE - # usedforsecurity=false: sha1 is not used cryptographically, and it's part of the protocol, so we use it as is.- if sha1(data, usedforsecurity=False).hexdigest() != checksum:- raise Exception("checksum mismatch")+ with tempfile.TemporaryDirectory() as tempdir:+ local_path = Path(tempdir, checksum) - write_storage = get_write_storage("file")- file, created = File.objects.get_or_create(- checksum=checksum,- defaults={- "size": len(data),- "data": b"" if write_storage is not None else data,- "filename": filename,- "storage_backend": None if write_storage is None else write_storage.name,- })+ # usedforsecurity=false: sha1 is not used cryptographically, and it's part of the protocol, so we use it as is.+ checksum_state = sha1(usedforsecurity=False)+ size = 0 - if created and write_storage is not None:- write_to_storage("file", checksum, data)+ with local_path.open("wb") as output_stream:+ for chunk in chunks_in_order:+ chunk_data = _binary_to_bytes(chunk.data)+ next_size = size + len(chunk_data)+ if next_size > max_file_size:+ raise ValueError("Assembled file exceeds MAX_FILE_SIZE") - # the assumption here is: chunks are basically use-once, so we can delete them after use. "in theory" a chunk may- # be used in multiple files (which are still being assembled) but with chunksizes in the order of 1MiB, I'd say this- # is unlikely.- chunks.delete()- return file, created@@ -197,22 +194,21 @@ max_file_size = get_settings().MAX_FILE_SIZE with tempfile.TemporaryDirectory() as tempdir:- local_path = Path(tempdir, checksum)-- # usedforsecurity=false: sha1 is not used cryptographically, and it's part of the protocol, so we use it as is.- checksum_state = sha1(usedforsecurity=False)- size = 0+ with tempfile.TemporaryFile(dir=tempdir) as local_file:+ # usedforsecurity=false: sha1 is not used cryptographically, and it's part of the protocol, so we use it+ # as is.+ checksum_state = sha1(usedforsecurity=False)+ size = 0 - with local_path.open("wb") as output_stream: for chunk in chunks_in_order: chunk_data = _binary_to_bytes(chunk.data) next_size = size + len(chunk_data) if next_size > max_file_size: raise ValueError("Assembled file exceeds MAX_FILE_SIZE") - output_stream.write(chunk_data)+ local_file.write(chunk_data) checksum_state.update(chunk_data) size = next_size - if checksum_state.hexdigest() != checksum:- raise Exception("checksum mismatch")+ if checksum_state.hexdigest() != checksum:+ raise Exception("checksum mismatch")AI-assisted change 46c6be853a6d18cddf85a8730e6d82bfaff27c49a1b2f26fec3df3320b6e2212 · Fix ce47465f498407f54f68952d473b14e463a19698866a5f4ba1af62bebe8fe6bb