fix(security): use crypto-random UUIDs for blob uploads
Replace timestamp-based blob naming with cryptographically secure UUIDs to prevent predictable file paths and potential security issues. Update validation regex and tests to reflect new UUID format.
- commit
- 02b51ba76905501ca645be71ea17de2f51a07362
- author
- brice · · 2026-02-06 05:32 UTC
- parent
- df25f46
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +31 -33
M internal/adapters/in/http/registry/handler_test.go +9 -9
| @@ -257,7 +257,7 @@ func TestHandler_StartBlobUpload_Success(t *testing.T) { | |||
| 257 | 257 | ||
| 258 | 258 | handler := NewHandler(registrySvc, testLogger()) | |
| 259 | 259 | ||
| 260 | - | registrySvc.EXPECT().StartUpload(mock.Anything, "myapp").Return("1234567890-myapp", nil) | |
| 260 | + | registrySvc.EXPECT().StartUpload(mock.Anything, "myapp").Return("550e8400-e29b-41d4-a716-446655440000", nil) | |
| 261 | 261 | ||
| 262 | 262 | req := httptest.NewRequest("POST", "/v2/myapp/blobs/uploads/", nil) | |
| 263 | 263 | rec := httptest.NewRecorder() | |
| @@ -265,7 +265,7 @@ func TestHandler_StartBlobUpload_Success(t *testing.T) { | |||
| 265 | 265 | handler.ServeHTTP(rec, req) | |
| 266 | 266 | ||
| 267 | 267 | assert.Equal(t, http.StatusAccepted, rec.Code) | |
| 268 | - | assert.Contains(t, rec.Header().Get("Location"), "/v2/myapp/blobs/uploads/1234567890-myapp") | |
| 268 | + | assert.Contains(t, rec.Header().Get("Location"), "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000") | |
| 269 | 269 | assert.Equal(t, "0-0", rec.Header().Get("Range")) | |
| 270 | 270 | } | |
| 271 | 271 | ||
| @@ -291,15 +291,15 @@ func TestHandler_BlobUpload_PATCH(t *testing.T) { | |||
| 291 | 291 | handler := NewHandler(registrySvc, testLogger()) | |
| 292 | 292 | ||
| 293 | 293 | chunkData := []byte("chunk content") | |
| 294 | - | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "1234567890-myapp", chunkData).Return(int64(len(chunkData)), nil) | |
| 294 | + | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", chunkData).Return(int64(len(chunkData)), nil) | |
| 295 | 295 | ||
| 296 | - | req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/1234567890-myapp", bytes.NewReader(chunkData)) | |
| 296 | + | req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader(chunkData)) | |
| 297 | 297 | rec := httptest.NewRecorder() | |
| 298 | 298 | ||
| 299 | 299 | handler.ServeHTTP(rec, req) | |
| 300 | 300 | ||
| 301 | 301 | assert.Equal(t, http.StatusAccepted, rec.Code) | |
| 302 | - | assert.Contains(t, rec.Header().Get("Location"), "/v2/myapp/blobs/uploads/1234567890-myapp") | |
| 302 | + | assert.Contains(t, rec.Header().Get("Location"), "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000") | |
| 303 | 303 | } | |
| 304 | 304 | ||
| 305 | 305 | func TestHandler_BlobUpload_PUT_Finalize(t *testing.T) { | |
| @@ -308,10 +308,10 @@ func TestHandler_BlobUpload_PUT_Finalize(t *testing.T) { | |||
| 308 | 308 | handler := NewHandler(registrySvc, testLogger()) | |
| 309 | 309 | ||
| 310 | 310 | chunkData := []byte("final chunk") | |
| 311 | - | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "1234567890-myapp", chunkData).Return(int64(len(chunkData)), nil) | |
| 312 | - | registrySvc.EXPECT().FinishUpload(mock.Anything, "1234567890-myapp", "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4").Return(nil) | |
| 311 | + | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", chunkData).Return(int64(len(chunkData)), nil) | |
| 312 | + | registrySvc.EXPECT().FinishUpload(mock.Anything, "550e8400-e29b-41d4-a716-446655440000", "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4").Return(nil) | |
| 313 | 313 | ||
| 314 | - | req := httptest.NewRequest("PUT", "/v2/myapp/blobs/uploads/1234567890-myapp?digest=sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", bytes.NewReader(chunkData)) | |
| 314 | + | req := httptest.NewRequest("PUT", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000?digest=sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", bytes.NewReader(chunkData)) | |
| 315 | 315 | rec := httptest.NewRecorder() | |
| 316 | 316 | ||
| 317 | 317 | handler.ServeHTTP(rec, req) | |
| @@ -330,7 +330,7 @@ func TestHandler_BlobUpload_PUT_DigestMismatch(t *testing.T) { | |||
| 330 | 330 | // Note: Invalid digest format is rejected by validation before reaching storage layer | |
| 331 | 331 | // This test verifies that invalid digests are properly rejected | |
| 332 | 332 | ||
| 333 | - | req := httptest.NewRequest("PUT", "/v2/myapp/blobs/uploads/1234567890-myapp?digest=sha256:wrong", bytes.NewReader(chunkData)) | |
| 333 | + | req := httptest.NewRequest("PUT", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000?digest=sha256:wrong", bytes.NewReader(chunkData)) | |
| 334 | 334 | rec := httptest.NewRecorder() | |
| 335 | 335 | ||
| 336 | 336 | handler.ServeHTTP(rec, req) | |
| @@ -257,7 +257,7 @@ func TestHandler_StartBlobUpload_Success(t *testing.T) { | |||||
| 257 | 257 | ||||
| 258 | handler := NewHandler(registrySvc, testLogger()) | 258 | handler := NewHandler(registrySvc, testLogger()) | ||
| 259 | 259 | ||||
| 260 | - | registrySvc.EXPECT().StartUpload(mock.Anything, "myapp").Return("1234567890-myapp", nil) | 260 | + | registrySvc.EXPECT().StartUpload(mock.Anything, "myapp").Return("550e8400-e29b-41d4-a716-446655440000", nil) |
| 261 | 261 | ||||
| 262 | req := httptest.NewRequest("POST", "/v2/myapp/blobs/uploads/", nil) | 262 | req := httptest.NewRequest("POST", "/v2/myapp/blobs/uploads/", nil) | ||
| 263 | rec := httptest.NewRecorder() | 263 | rec := httptest.NewRecorder() | ||
| @@ -265,7 +265,7 @@ func TestHandler_StartBlobUpload_Success(t *testing.T) { | |||||
| 265 | handler.ServeHTTP(rec, req) | 265 | handler.ServeHTTP(rec, req) | ||
| 266 | 266 | ||||
| 267 | assert.Equal(t, http.StatusAccepted, rec.Code) | 267 | assert.Equal(t, http.StatusAccepted, rec.Code) | ||
| 268 | - | assert.Contains(t, rec.Header().Get("Location"), "/v2/myapp/blobs/uploads/1234567890-myapp") | 268 | + | assert.Contains(t, rec.Header().Get("Location"), "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000") |
| 269 | assert.Equal(t, "0-0", rec.Header().Get("Range")) | 269 | assert.Equal(t, "0-0", rec.Header().Get("Range")) | ||
| 270 | } | 270 | } | ||
| 271 | 271 | ||||
| @@ -291,15 +291,15 @@ func TestHandler_BlobUpload_PATCH(t *testing.T) { | |||||
| 291 | handler := NewHandler(registrySvc, testLogger()) | 291 | handler := NewHandler(registrySvc, testLogger()) | ||
| 292 | 292 | ||||
| 293 | chunkData := []byte("chunk content") | 293 | chunkData := []byte("chunk content") | ||
| 294 | - | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "1234567890-myapp", chunkData).Return(int64(len(chunkData)), nil) | 294 | + | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", chunkData).Return(int64(len(chunkData)), nil) |
| 295 | 295 | ||||
| 296 | - | req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/1234567890-myapp", bytes.NewReader(chunkData)) | 296 | + | req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader(chunkData)) |
| 297 | rec := httptest.NewRecorder() | 297 | rec := httptest.NewRecorder() | ||
| 298 | 298 | ||||
| 299 | handler.ServeHTTP(rec, req) | 299 | handler.ServeHTTP(rec, req) | ||
| 300 | 300 | ||||
| 301 | assert.Equal(t, http.StatusAccepted, rec.Code) | 301 | assert.Equal(t, http.StatusAccepted, rec.Code) | ||
| 302 | - | assert.Contains(t, rec.Header().Get("Location"), "/v2/myapp/blobs/uploads/1234567890-myapp") | 302 | + | assert.Contains(t, rec.Header().Get("Location"), "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000") |
| 303 | } | 303 | } | ||
| 304 | 304 | ||||
| 305 | func TestHandler_BlobUpload_PUT_Finalize(t *testing.T) { | 305 | func TestHandler_BlobUpload_PUT_Finalize(t *testing.T) { | ||
| @@ -308,10 +308,10 @@ func TestHandler_BlobUpload_PUT_Finalize(t *testing.T) { | |||||
| 308 | handler := NewHandler(registrySvc, testLogger()) | 308 | handler := NewHandler(registrySvc, testLogger()) | ||
| 309 | 309 | ||||
| 310 | chunkData := []byte("final chunk") | 310 | chunkData := []byte("final chunk") | ||
| 311 | - | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "1234567890-myapp", chunkData).Return(int64(len(chunkData)), nil) | 311 | + | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", chunkData).Return(int64(len(chunkData)), nil) |
| 312 | - | registrySvc.EXPECT().FinishUpload(mock.Anything, "1234567890-myapp", "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4").Return(nil) | 312 | + | registrySvc.EXPECT().FinishUpload(mock.Anything, "550e8400-e29b-41d4-a716-446655440000", "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4").Return(nil) |
| 313 | 313 | ||||
| 314 | - | req := httptest.NewRequest("PUT", "/v2/myapp/blobs/uploads/1234567890-myapp?digest=sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", bytes.NewReader(chunkData)) | 314 | + | req := httptest.NewRequest("PUT", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000?digest=sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", bytes.NewReader(chunkData)) |
| 315 | rec := httptest.NewRecorder() | 315 | rec := httptest.NewRecorder() | ||
| 316 | 316 | ||||
| 317 | handler.ServeHTTP(rec, req) | 317 | handler.ServeHTTP(rec, req) | ||
| @@ -330,7 +330,7 @@ func TestHandler_BlobUpload_PUT_DigestMismatch(t *testing.T) { | |||||
| 330 | // Note: Invalid digest format is rejected by validation before reaching storage layer | 330 | // Note: Invalid digest format is rejected by validation before reaching storage layer | ||
| 331 | // This test verifies that invalid digests are properly rejected | 331 | // This test verifies that invalid digests are properly rejected | ||
| 332 | 332 | ||||
| 333 | - | req := httptest.NewRequest("PUT", "/v2/myapp/blobs/uploads/1234567890-myapp?digest=sha256:wrong", bytes.NewReader(chunkData)) | 333 | + | req := httptest.NewRequest("PUT", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000?digest=sha256:wrong", bytes.NewReader(chunkData)) |
| 334 | rec := httptest.NewRecorder() | 334 | rec := httptest.NewRecorder() | ||
| 335 | 335 | ||||
| 336 | handler.ServeHTTP(rec, req) | 336 | handler.ServeHTTP(rec, req) | ||
M internal/adapters/out/filesystem/blob.go +5 -6
| @@ -11,9 +11,9 @@ import ( | |||
| 11 | 11 | "os" | |
| 12 | 12 | "path/filepath" | |
| 13 | 13 | "strings" | |
| 14 | - | "time" | |
| 15 | 14 | ||
| 16 | 15 | "github.com/bnema/zerowrap" | |
| 16 | + | "github.com/google/uuid" | |
| 17 | 17 | ||
| 18 | 18 | "github.com/bnema/gordon/pkg/validation" | |
| 19 | 19 | ) | |
| @@ -167,10 +167,9 @@ func (s *BlobStorage) BlobExists(digest string) bool { | |||
| 167 | 167 | ||
| 168 | 168 | // StartBlobUpload starts a new blob upload and returns the upload UUID. | |
| 169 | 169 | func (s *BlobStorage) StartBlobUpload(name string) (string, error) { | |
| 170 | - | // Generate UUID-like upload ID using timestamp | |
| 171 | - | uuid := fmt.Sprintf("%d-%s", time.Now().UnixNano(), strings.ReplaceAll(name, "/", "_")) | |
| 170 | + | uploadID := uuid.New().String() | |
| 172 | 171 | ||
| 173 | - | uploadPath, err := s.getUploadPath(uuid) | |
| 172 | + | uploadPath, err := s.getUploadPath(uploadID) | |
| 174 | 173 | if err != nil { | |
| 175 | 174 | return "", fmt.Errorf("invalid upload path: %w", err) | |
| 176 | 175 | } | |
| @@ -190,11 +189,11 @@ func (s *BlobStorage) StartBlobUpload(name string) (string, error) { | |||
| 190 | 189 | s.log.Info(). | |
| 191 | 190 | Str(zerowrap.FieldLayer, "adapter"). | |
| 192 | 191 | Str(zerowrap.FieldAdapter, "filesystem"). | |
| 193 | - | Str("uuid", uuid). | |
| 192 | + | Str("uuid", uploadID). | |
| 194 | 193 | Str("name", name). | |
| 195 | 194 | Msg("blob upload started") | |
| 196 | 195 | ||
| 197 | - | return uuid, nil | |
| 196 | + | return uploadID, nil | |
| 198 | 197 | } | |
| 199 | 198 | ||
| 200 | 199 | // AppendBlobChunk appends data to an in-progress upload. | |
| @@ -11,9 +11,9 @@ import ( | |||||
| 11 | "os" | 11 | "os" | ||
| 12 | "path/filepath" | 12 | "path/filepath" | ||
| 13 | "strings" | 13 | "strings" | ||
| 14 | - | "time" | |||
| 15 | 14 | ||||
| 16 | "github.com/bnema/zerowrap" | 15 | "github.com/bnema/zerowrap" | ||
| 16 | + | "github.com/google/uuid" | |||
| 17 | 17 | ||||
| 18 | "github.com/bnema/gordon/pkg/validation" | 18 | "github.com/bnema/gordon/pkg/validation" | ||
| 19 | ) | 19 | ) | ||
| @@ -167,10 +167,9 @@ func (s *BlobStorage) BlobExists(digest string) bool { | |||||
| 167 | 167 | ||||
| 168 | // StartBlobUpload starts a new blob upload and returns the upload UUID. | 168 | // StartBlobUpload starts a new blob upload and returns the upload UUID. | ||
| 169 | func (s *BlobStorage) StartBlobUpload(name string) (string, error) { | 169 | func (s *BlobStorage) StartBlobUpload(name string) (string, error) { | ||
| 170 | - | // Generate UUID-like upload ID using timestamp | 170 | + | uploadID := uuid.New().String() |
| 171 | - | uuid := fmt.Sprintf("%d-%s", time.Now().UnixNano(), strings.ReplaceAll(name, "/", "_")) | |||
| 172 | 171 | ||||
| 173 | - | uploadPath, err := s.getUploadPath(uuid) | 172 | + | uploadPath, err := s.getUploadPath(uploadID) |
| 174 | if err != nil { | 173 | if err != nil { | ||
| 175 | return "", fmt.Errorf("invalid upload path: %w", err) | 174 | return "", fmt.Errorf("invalid upload path: %w", err) | ||
| 176 | } | 175 | } | ||
| @@ -190,11 +189,11 @@ func (s *BlobStorage) StartBlobUpload(name string) (string, error) { | |||||
| 190 | s.log.Info(). | 189 | s.log.Info(). | ||
| 191 | Str(zerowrap.FieldLayer, "adapter"). | 190 | Str(zerowrap.FieldLayer, "adapter"). | ||
| 192 | Str(zerowrap.FieldAdapter, "filesystem"). | 191 | Str(zerowrap.FieldAdapter, "filesystem"). | ||
| 193 | - | Str("uuid", uuid). | 192 | + | Str("uuid", uploadID). |
| 194 | Str("name", name). | 193 | Str("name", name). | ||
| 195 | Msg("blob upload started") | 194 | Msg("blob upload started") | ||
| 196 | 195 | ||||
| 197 | - | return uuid, nil | 196 | + | return uploadID, nil |
| 198 | } | 197 | } | ||
| 199 | 198 | ||||
| 200 | // AppendBlobChunk appends data to an in-progress upload. | 199 | // AppendBlobChunk appends data to an in-progress upload. | ||
M internal/adapters/out/filesystem/blob_test.go +4 -4
| @@ -222,7 +222,7 @@ func TestBlobStorage_StartBlobUpload(t *testing.T) { | |||
| 222 | 222 | ||
| 223 | 223 | require.NoError(t, err) | |
| 224 | 224 | assert.NotEmpty(t, uuid) | |
| 225 | - | assert.Contains(t, uuid, "myapp") | |
| 225 | + | assert.Len(t, uuid, 36) // standard UUID format | |
| 226 | 226 | ||
| 227 | 227 | // Verify upload file was created | |
| 228 | 228 | uploadPath := filepath.Join(tmpDir, "uploads", uuid) | |
| @@ -260,7 +260,7 @@ func TestBlobStorage_AppendBlobChunk_NotFound(t *testing.T) { | |||
| 260 | 260 | storage, err := NewBlobStorage(tmpDir, log) | |
| 261 | 261 | require.NoError(t, err) | |
| 262 | 262 | ||
| 263 | - | _, err = storage.AppendBlobChunk("myapp", "1234567890-invalid", []byte("data")) | |
| 263 | + | _, err = storage.AppendBlobChunk("myapp", "00000000-0000-0000-0000-000000000000", []byte("data")) | |
| 264 | 264 | ||
| 265 | 265 | assert.Error(t, err) | |
| 266 | 266 | assert.Contains(t, err.Error(), "upload not found") | |
| @@ -292,7 +292,7 @@ func TestBlobStorage_GetBlobUpload_NotFound(t *testing.T) { | |||
| 292 | 292 | storage, err := NewBlobStorage(tmpDir, log) | |
| 293 | 293 | require.NoError(t, err) | |
| 294 | 294 | ||
| 295 | - | writer, err := storage.GetBlobUpload("1234567890-invalid") | |
| 295 | + | writer, err := storage.GetBlobUpload("00000000-0000-0000-0000-000000000000") | |
| 296 | 296 | ||
| 297 | 297 | assert.Error(t, err) | |
| 298 | 298 | assert.Nil(t, writer) | |
| @@ -355,7 +355,7 @@ func TestBlobStorage_CancelBlobUpload_NotFound(t *testing.T) { | |||
| 355 | 355 | storage, err := NewBlobStorage(tmpDir, log) | |
| 356 | 356 | require.NoError(t, err) | |
| 357 | 357 | ||
| 358 | - | err = storage.CancelBlobUpload("1234567890-invalid") | |
| 358 | + | err = storage.CancelBlobUpload("00000000-0000-0000-0000-000000000000") | |
| 359 | 359 | ||
| 360 | 360 | assert.Error(t, err) | |
| 361 | 361 | assert.Contains(t, err.Error(), "upload not found") | |
| @@ -222,7 +222,7 @@ func TestBlobStorage_StartBlobUpload(t *testing.T) { | |||||
| 222 | 222 | ||||
| 223 | require.NoError(t, err) | 223 | require.NoError(t, err) | ||
| 224 | assert.NotEmpty(t, uuid) | 224 | assert.NotEmpty(t, uuid) | ||
| 225 | - | assert.Contains(t, uuid, "myapp") | 225 | + | assert.Len(t, uuid, 36) // standard UUID format |
| 226 | 226 | ||||
| 227 | // Verify upload file was created | 227 | // Verify upload file was created | ||
| 228 | uploadPath := filepath.Join(tmpDir, "uploads", uuid) | 228 | uploadPath := filepath.Join(tmpDir, "uploads", uuid) | ||
| @@ -260,7 +260,7 @@ func TestBlobStorage_AppendBlobChunk_NotFound(t *testing.T) { | |||||
| 260 | storage, err := NewBlobStorage(tmpDir, log) | 260 | storage, err := NewBlobStorage(tmpDir, log) | ||
| 261 | require.NoError(t, err) | 261 | require.NoError(t, err) | ||
| 262 | 262 | ||||
| 263 | - | _, err = storage.AppendBlobChunk("myapp", "1234567890-invalid", []byte("data")) | 263 | + | _, err = storage.AppendBlobChunk("myapp", "00000000-0000-0000-0000-000000000000", []byte("data")) |
| 264 | 264 | ||||
| 265 | assert.Error(t, err) | 265 | assert.Error(t, err) | ||
| 266 | assert.Contains(t, err.Error(), "upload not found") | 266 | assert.Contains(t, err.Error(), "upload not found") | ||
| @@ -292,7 +292,7 @@ func TestBlobStorage_GetBlobUpload_NotFound(t *testing.T) { | |||||
| 292 | storage, err := NewBlobStorage(tmpDir, log) | 292 | storage, err := NewBlobStorage(tmpDir, log) | ||
| 293 | require.NoError(t, err) | 293 | require.NoError(t, err) | ||
| 294 | 294 | ||||
| 295 | - | writer, err := storage.GetBlobUpload("1234567890-invalid") | 295 | + | writer, err := storage.GetBlobUpload("00000000-0000-0000-0000-000000000000") |
| 296 | 296 | ||||
| 297 | assert.Error(t, err) | 297 | assert.Error(t, err) | ||
| 298 | assert.Nil(t, writer) | 298 | assert.Nil(t, writer) | ||
| @@ -355,7 +355,7 @@ func TestBlobStorage_CancelBlobUpload_NotFound(t *testing.T) { | |||||
| 355 | storage, err := NewBlobStorage(tmpDir, log) | 355 | storage, err := NewBlobStorage(tmpDir, log) | ||
| 356 | require.NoError(t, err) | 356 | require.NoError(t, err) | ||
| 357 | 357 | ||||
| 358 | - | err = storage.CancelBlobUpload("1234567890-invalid") | 358 | + | err = storage.CancelBlobUpload("00000000-0000-0000-0000-000000000000") |
| 359 | 359 | ||||
| 360 | assert.Error(t, err) | 360 | assert.Error(t, err) | ||
| 361 | assert.Contains(t, err.Error(), "upload not found") | 361 | assert.Contains(t, err.Error(), "upload not found") | ||
M pkg/validation/path.go +2 -2
| @@ -28,8 +28,8 @@ var referenceRegex = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$`) | |||
| 28 | 28 | var digestRegex = regexp.MustCompile(`^(sha256:[a-f0-9]{64}|sha512:[a-f0-9]{128})$`) | |
| 29 | 29 | ||
| 30 | 30 | // UUID validation for blob uploads: | |
| 31 | - | // - Format: timestamp-name (where name has / replaced with _) | |
| 32 | - | var uuidRegex = regexp.MustCompile(`^[0-9]+-[a-zA-Z0-9_-]+$`) | |
| 31 | + | // - Format: standard UUID v4 (e.g., 550e8400-e29b-41d4-a716-446655440000) | |
| 32 | + | var uuidRegex = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`) | |
| 33 | 33 | ||
| 34 | 34 | // MaxRepositoryNameLength is the maximum allowed length for repository names. | |
| 35 | 35 | const MaxRepositoryNameLength = 256 | |
| @@ -28,8 +28,8 @@ var referenceRegex = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$`) | |||||
| 28 | var digestRegex = regexp.MustCompile(`^(sha256:[a-f0-9]{64}|sha512:[a-f0-9]{128})$`) | 28 | var digestRegex = regexp.MustCompile(`^(sha256:[a-f0-9]{64}|sha512:[a-f0-9]{128})$`) | ||
| 29 | 29 | ||||
| 30 | // UUID validation for blob uploads: | 30 | // UUID validation for blob uploads: | ||
| 31 | - | // - Format: timestamp-name (where name has / replaced with _) | 31 | + | // - Format: standard UUID v4 (e.g., 550e8400-e29b-41d4-a716-446655440000) |
| 32 | - | var uuidRegex = regexp.MustCompile(`^[0-9]+-[a-zA-Z0-9_-]+$`) | 32 | + | var uuidRegex = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`) |
| 33 | 33 | ||||
| 34 | // MaxRepositoryNameLength is the maximum allowed length for repository names. | 34 | // MaxRepositoryNameLength is the maximum allowed length for repository names. | ||
| 35 | const MaxRepositoryNameLength = 256 | 35 | const MaxRepositoryNameLength = 256 | ||
M pkg/validation/path_test.go +11 -12
| @@ -163,23 +163,22 @@ func TestValidateUUID(t *testing.T) { | |||
| 163 | 163 | wantErr bool | |
| 164 | 164 | errMsg string | |
| 165 | 165 | }{ | |
| 166 | - | // Valid cases | |
| 167 | - | {"valid uuid", "1234567890-myapp", false, ""}, | |
| 168 | - | {"with underscore", "1234567890-my_app", false, ""}, | |
| 169 | - | {"with hyphen in name", "1234567890-my-app", false, ""}, | |
| 170 | - | {"nested repo uuid", "1234567890-myorg_myapp", false, ""}, | |
| 166 | + | // Valid cases - standard UUID v4 | |
| 167 | + | {"valid uuid", "550e8400-e29b-41d4-a716-446655440000", false, ""}, | |
| 168 | + | {"another valid uuid", "6ba7b810-9dad-11d1-80b4-00c04fd430c8", false, ""}, | |
| 169 | + | {"all zeros", "00000000-0000-0000-0000-000000000000", false, ""}, | |
| 171 | 170 | ||
| 172 | 171 | // Invalid cases - path traversal | |
| 173 | - | {"path traversal", "1234567890-../etc/passwd", true, "path traversal"}, | |
| 174 | - | {"traversal in middle", "1234567890-app/../etc", true, "path traversal"}, | |
| 172 | + | {"path traversal", "../etc/passwd", true, "path traversal"}, | |
| 173 | + | {"traversal in middle", "app/../etc", true, "path traversal"}, | |
| 175 | 174 | ||
| 176 | 175 | // Invalid cases - format | |
| 177 | 176 | {"empty", "", true, "cannot be empty"}, | |
| 178 | - | {"no timestamp", "myapp", true, "invalid UUID format"}, | |
| 179 | - | {"no hyphen", "1234567890myapp", true, "invalid UUID format"}, | |
| 180 | - | {"special chars", "1234567890-my@app", true, "invalid UUID format"}, | |
| 181 | - | {"spaces", "1234567890-my app", true, "invalid UUID format"}, | |
| 182 | - | {"slash not replaced", "1234567890-myorg/myapp", true, "invalid UUID format"}, | |
| 177 | + | {"random string", "myapp", true, "invalid UUID format"}, | |
| 178 | + | {"old timestamp format", "1234567890-myapp", true, "invalid UUID format"}, | |
| 179 | + | {"uppercase hex", "550E8400-E29B-41D4-A716-446655440000", true, "invalid UUID format"}, | |
| 180 | + | {"spaces", "550e8400 e29b 41d4 a716 446655440000", true, "invalid UUID format"}, | |
| 181 | + | {"missing section", "550e8400-e29b-41d4-a716", true, "invalid UUID format"}, | |
| 183 | 182 | } | |
| 184 | 183 | ||
| 185 | 184 | for _, tt := range tests { | |
| @@ -163,23 +163,22 @@ func TestValidateUUID(t *testing.T) { | |||||
| 163 | wantErr bool | 163 | wantErr bool | ||
| 164 | errMsg string | 164 | errMsg string | ||
| 165 | }{ | 165 | }{ | ||
| 166 | - | // Valid cases | 166 | + | // Valid cases - standard UUID v4 |
| 167 | - | {"valid uuid", "1234567890-myapp", false, ""}, | 167 | + | {"valid uuid", "550e8400-e29b-41d4-a716-446655440000", false, ""}, |
| 168 | - | {"with underscore", "1234567890-my_app", false, ""}, | 168 | + | {"another valid uuid", "6ba7b810-9dad-11d1-80b4-00c04fd430c8", false, ""}, |
| 169 | - | {"with hyphen in name", "1234567890-my-app", false, ""}, | 169 | + | {"all zeros", "00000000-0000-0000-0000-000000000000", false, ""}, |
| 170 | - | {"nested repo uuid", "1234567890-myorg_myapp", false, ""}, | |||
| 171 | 170 | ||||
| 172 | // Invalid cases - path traversal | 171 | // Invalid cases - path traversal | ||
| 173 | - | {"path traversal", "1234567890-../etc/passwd", true, "path traversal"}, | 172 | + | {"path traversal", "../etc/passwd", true, "path traversal"}, |
| 174 | - | {"traversal in middle", "1234567890-app/../etc", true, "path traversal"}, | 173 | + | {"traversal in middle", "app/../etc", true, "path traversal"}, |
| 175 | 174 | ||||
| 176 | // Invalid cases - format | 175 | // Invalid cases - format | ||
| 177 | {"empty", "", true, "cannot be empty"}, | 176 | {"empty", "", true, "cannot be empty"}, | ||
| 178 | - | {"no timestamp", "myapp", true, "invalid UUID format"}, | 177 | + | {"random string", "myapp", true, "invalid UUID format"}, |
| 179 | - | {"no hyphen", "1234567890myapp", true, "invalid UUID format"}, | 178 | + | {"old timestamp format", "1234567890-myapp", true, "invalid UUID format"}, |
| 180 | - | {"special chars", "1234567890-my@app", true, "invalid UUID format"}, | 179 | + | {"uppercase hex", "550E8400-E29B-41D4-A716-446655440000", true, "invalid UUID format"}, |
| 181 | - | {"spaces", "1234567890-my app", true, "invalid UUID format"}, | 180 | + | {"spaces", "550e8400 e29b 41d4 a716 446655440000", true, "invalid UUID format"}, |
| 182 | - | {"slash not replaced", "1234567890-myorg/myapp", true, "invalid UUID format"}, | 181 | + | {"missing section", "550e8400-e29b-41d4-a716", true, "invalid UUID format"}, |
| 183 | } | 182 | } | ||
| 184 | 183 | ||||
| 185 | for _, tt := range tests { | 184 | for _, tt := range tests { | ||