File Upload & Storage Security
Implemented Controls
Storage Architecture
lib/git_gud/storage/
Storage.Disk— Local filesystem storageStorage.Garage— S3-compatible object storage- Unified interface for put/get/delete/presign operations
- No multipart upload support — assembly happens in memory for chunked uploads
Storage Controller
lib/git_gud_web/controllers/storage_controller.ex
- Signed/capability tokens for file access
Disk.verify_token/2— validates token before file access- Token contains bucket + key binding (prevents path traversal)
- Presigned URLs with configurable expiry
Upload Size Limits
| Component | Limit | File/Line |
|---|---|---|
| Runner proto body | 50MB | runner_controller.ex:700 |
| Artifact upload | 1GB | actions_artifact_controller.ex:76 |
| Cache chunk upload | 200MB | actions_cache_controller.ex:91 |
| Package upload | 200MB | PackageRegistryController |
| LFS objects | Configurable | LfsController |
Runner Artifact Storage
lib/git_gud_web/controllers/actions_artifact_controller.ex
- Authenticated by runner JWT (
RunnerJwtAuthplug) - Run-scoped: job only accesses its own run’s artifacts (
own_run/2) - Storage key sanitized:
String.replace(name, ~r/[^A-Za-z0-9._\-]/, "_") - Storage path:
gitgud-artifacts/run-<run_id>/<sanitized_name>
Runner Cache Storage
lib/git_gud_web/controllers/actions_cache_controller.ex
- Authenticated by runner JWT
- Entry-scoped: only the job that reserved can upload (
reserved_by?/2) - Upload expiry: 600 seconds (10 minutes)
- Chunk validation:
Content-Rangeheader parsed and validated - Finalization: chunks assembled in offset order, no gaps allowed
Package Registry Uploads
lib/git_gud_web/controllers/package_registry_controller.ex
- Size limit: 200MB per upload
- Content-type validation based on package format
- Authentication via registry token or PAT
- Format-specific parsing (
Formats.Deb,Formats.Rpm,Formats.Hex, etc.)
Security Observations
High
No file type validation in storage controller
StorageControllerrelies on upstream validation- No magic byte checking (e.g., PE/ELF/Mach-O for binaries)
- No MIME type verification against declared Content-Type
- Recommendation: add content-type validation based on actual file content
- Use
:erlang.md5/1or similar to check magic bytes for known formats
No virus/malware scanning
- Uploaded artifacts, packages, and LFS objects not scanned
- Malicious binaries can be distributed through the forge
- Recommendation: integrate malware scanner (e.g., ClamAV)
- Scan on upload, quarantine flagged files
- Consider async scanning with upload allowed pending scan
Large body reads can cause memory exhaustion
actions_artifact_controller.ex:76— 1GB read per upload- Multiple concurrent uploads can exhaust server memory
- Recommendation: add per-user concurrent upload limits
- Consider streaming uploads directly to object storage
actions_cache_controller.ex:219— Cache finalization assembles chunks in memory- Large cache entries loaded entirely into memory
- Recommendation: implement streaming assembly or limit max cache entry size
Medium
Content-type spoofing in artifact downloads
actions_artifact_controller.ex:150— Downloads returnartifact.content_typefrom database- Attacker can set arbitrary
content_typeduring artifact creation - If artifacts served inline in browser, could enable XSS via HTML/JS content types
- Recommendation: validate and sanitize
content_typefield - Default to
application/octet-streamunless explicitly set to safe MIME type - Add allowlist of allowed content types
Cache chunk upload not strictly validated
actions_cache_controller.ex:87-109— Upload validatesContent-Rangeheader- No validation that chunk offset matches expected position
- Recommendation: add server-side offset tracking per upload session
- Reject chunks that don’t match expected sequence
Presigned URL expiry too long
actions_cache.ex:46— Download expiry: 300 secondsactions_cache.ex:47— Upload expiry: 600 seconds- Long expiry increases window for URL theft/misuse
- Recommendation: reduce download expiry to 60 seconds
- Keep upload expiry at 300 seconds (already short)
No upload checksums
- Uploaded files not verified against client-provided checksums
- Corruption during upload not detected
- Recommendation: accept
Content-MD5header from clients - Verify checksum after upload, return error on mismatch
Low
Upload URL construction
actions_artifact_controller.ex:184-187— Upload URLs constructed via string interpolation- No validation that
endpoint.url()is safe - Recommendation: use Phoenix URL helpers for URL generation
- Prevent URL scheme injection
No rate limiting on uploads
- Upload endpoints have no rate limiting
- Can be used for storage exhaustion attacks
- Recommendation: add per-user upload limits (e.g., 10 uploads/hour)
- Consider storage quota per repository/organization