Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"gitea.dev/models/migrations/v1_25"
"gitea.dev/models/migrations/v1_26"
"gitea.dev/models/migrations/v1_27"
"gitea.dev/models/migrations/v1_28"
"gitea.dev/models/migrations/v1_6"
"gitea.dev/models/migrations/v1_7"
"gitea.dev/models/migrations/v1_8"
Expand Down Expand Up @@ -420,6 +421,9 @@ func prepareMigrationTasks() []*migration {
newMigration(340, "Add ContinueOnError column to ActionRunJob", v1_27.AddContinueOnErrorToActionRunJob),
newMigration(341, "Convert legacy MSSQL DATETIME columns to DATETIME2", v1_27.FixLegacyMSSQLDateTimeColumns),
newMigration(342, "Add scoped workflows schema", v1_27.AddScopedWorkflowsSchema),
// Gitea 1.27.0 ends at migration ID number 342 (database version 343)

newMigration(343, "Add creator_id to package blob uploads", v1_28.AddCreatorIDToPackageBlobUpload),
}
return preparedMigrations
}
Expand Down
14 changes: 14 additions & 0 deletions models/migrations/v1_28/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package v1_28

import (
"testing"

"gitea.dev/models/migrations/migrationtest"
)

func TestMain(m *testing.M) {
migrationtest.MainTest(m)
}
24 changes: 24 additions & 0 deletions models/migrations/v1_28/v343.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package v1_28

import (
"gitea.dev/models/db"

"xorm.io/xorm"
)

// AddCreatorIDToPackageBlobUpload binds blob upload sessions to the user who
// created them so other authenticated users cannot reuse a leaked UUID.
func AddCreatorIDToPackageBlobUpload(x db.EngineMigration) error {
type PackageBlobUpload struct {
CreatorID int64 `xorm:"INDEX NOT NULL DEFAULT 0"`
}

_, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreDropIndices: true,
IgnoreConstrains: true,
}, new(PackageBlobUpload))
return err
}
51 changes: 51 additions & 0 deletions models/migrations/v1_28/v343_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package v1_28

import (
"testing"

"gitea.dev/models/migrations/migrationtest"

"github.com/stretchr/testify/require"
)

type packageBlobUploadBeforeV343 struct {
ID string `xorm:"pk"`
BytesReceived int64 `xorm:"NOT NULL DEFAULT 0"`
}

func (packageBlobUploadBeforeV343) TableName() string {
return "package_blob_upload"
}

type packageBlobUploadAfterV343 struct {
ID string `xorm:"pk"`
CreatorID int64 `xorm:"INDEX NOT NULL DEFAULT 0"`
BytesReceived int64 `xorm:"NOT NULL DEFAULT 0"`
}

func (packageBlobUploadAfterV343) TableName() string {
return "package_blob_upload"
}

func Test_AddCreatorIDToPackageBlobUpload(t *testing.T) {
x, deferable := migrationtest.PrepareTestEnv(t, 0, new(packageBlobUploadBeforeV343))
defer deferable()

_, err := x.Insert(&packageBlobUploadBeforeV343{
ID: "test-upload",
BytesReceived: 12,
})
require.NoError(t, err)

require.NoError(t, AddCreatorIDToPackageBlobUpload(x))

var pbu packageBlobUploadAfterV343
has, err := x.ID("test-upload").Get(&pbu)
require.NoError(t, err)
require.True(t, has)
require.EqualValues(t, 0, pbu.CreatorID)
require.EqualValues(t, 12, pbu.BytesReceived)
}
10 changes: 6 additions & 4 deletions models/packages/package_blob_upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,29 +23,31 @@ func init() {
// PackageBlobUpload represents a package blob upload
type PackageBlobUpload struct {
ID string `xorm:"pk"`
CreatorID int64 `xorm:"INDEX NOT NULL DEFAULT 0"`
BytesReceived int64 `xorm:"NOT NULL DEFAULT 0"`
HashStateBytes []byte `xorm:"BLOB"`
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated INDEX NOT NULL"`
}

// CreateBlobUpload inserts a blob upload
func CreateBlobUpload(ctx context.Context) (*PackageBlobUpload, error) {
func CreateBlobUpload(ctx context.Context, creatorID int64) (*PackageBlobUpload, error) {
id := util.CryptoRandomString(25)

pbu := &PackageBlobUpload{
ID: strings.ToLower(id),
ID: strings.ToLower(id),
CreatorID: creatorID,
}

_, err := db.GetEngine(ctx).Insert(pbu)
return pbu, err
}

// GetBlobUploadByID gets a blob upload by id
func GetBlobUploadByID(ctx context.Context, id string) (*PackageBlobUpload, error) {
func GetBlobUploadByID(ctx context.Context, id string, creatorID int64) (*PackageBlobUpload, error) {
pbu := &PackageBlobUpload{}

has, err := db.GetEngine(ctx).ID(id).Get(pbu)
has, err := db.GetEngine(ctx).Where("id = ? AND creator_id = ?", id, creatorID).Get(pbu)
if err != nil {
return nil, err
}
Expand Down
10 changes: 5 additions & 5 deletions routers/api/packages/container/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ func PostBlobsUploads(ctx *context.Context) {
return
}

upload, err := packages_model.CreateBlobUpload(ctx)
upload, err := packages_model.CreateBlobUpload(ctx, ctx.Doer.ID)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
Expand All @@ -336,7 +336,7 @@ func GetBlobsUpload(ctx *context.Context) {
image := ctx.PathParam("image")
uuid := ctx.PathParam("uuid")

upload, err := packages_model.GetBlobUploadByID(ctx, uuid)
upload, err := packages_model.GetBlobUploadByID(ctx, uuid, ctx.Doer.ID)
if err != nil {
if errors.Is(err, packages_model.ErrPackageBlobUploadNotExist) {
apiErrorDefined(ctx, errBlobUploadUnknown)
Expand All @@ -363,7 +363,7 @@ func GetBlobsUpload(ctx *context.Context) {
func PatchBlobsUpload(ctx *context.Context) {
image := ctx.PathParam("image")

uploader, err := container_service.NewBlobUploader(ctx, ctx.PathParam("uuid"))
uploader, err := container_service.NewBlobUploader(ctx, ctx.PathParam("uuid"), ctx.Doer.ID)
if err != nil {
if errors.Is(err, packages_model.ErrPackageBlobUploadNotExist) {
apiErrorDefined(ctx, errBlobUploadUnknown)
Expand Down Expand Up @@ -417,7 +417,7 @@ func PutBlobsUpload(ctx *context.Context) {
return
}

uploader, err := container_service.NewBlobUploader(ctx, ctx.PathParam("uuid"))
uploader, err := container_service.NewBlobUploader(ctx, ctx.PathParam("uuid"), ctx.Doer.ID)
if err != nil {
if errors.Is(err, packages_model.ErrPackageBlobUploadNotExist) {
apiErrorDefined(ctx, errBlobUploadUnknown)
Expand Down Expand Up @@ -480,7 +480,7 @@ func PutBlobsUpload(ctx *context.Context) {
func DeleteBlobsUpload(ctx *context.Context) {
uuid := ctx.PathParam("uuid")

_, err := packages_model.GetBlobUploadByID(ctx, uuid)
_, err := packages_model.GetBlobUploadByID(ctx, uuid, ctx.Doer.ID)
if err != nil {
if errors.Is(err, packages_model.ErrPackageBlobUploadNotExist) {
apiErrorDefined(ctx, errBlobUploadUnknown)
Expand Down
4 changes: 2 additions & 2 deletions services/packages/container/blob_uploader.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ func buildFilePath(uploadPath *tempdir.TempDir, id string) string {
}

// NewBlobUploader creates a new blob uploader for the given id
func NewBlobUploader(ctx context.Context, id string) (*BlobUploader, error) {
model, err := packages_model.GetBlobUploadByID(ctx, id)
func NewBlobUploader(ctx context.Context, id string, creatorID int64) (*BlobUploader, error) {
model, err := packages_model.GetBlobUploadByID(ctx, id, creatorID)
if err != nil {
return nil, err
}
Expand Down
22 changes: 20 additions & 2 deletions tests/integration/api_packages_container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,13 +274,31 @@ func TestPackageContainer(t *testing.T) {
uuid := resp.Header().Get("Docker-Upload-Uuid")
assert.NotEmpty(t, uuid)

pbu, err := packages_model.GetBlobUploadByID(t.Context(), uuid)
pbu, err := packages_model.GetBlobUploadByID(t.Context(), uuid, user.ID)
assert.NoError(t, err)
assert.EqualValues(t, 0, pbu.BytesReceived)

uploadURL := resp.Header().Get("Location")
assert.NotEmpty(t, uploadURL)

attackerUploadURL := fmt.Sprintf("%sv2/%s/%s/blobs/uploads/%s", setting.AppURL, privateUser.Name, image, uuid)

req = NewRequest(t, "GET", attackerUploadURL).
AddBasicAuth(privateUser.Name)
MakeRequest(t, req, http.StatusNotFound)

req = NewRequestWithBody(t, "PATCH", attackerUploadURL, bytes.NewReader([]byte("x"))).
AddBasicAuth(privateUser.Name)
MakeRequest(t, req, http.StatusNotFound)

req = NewRequest(t, "PUT", fmt.Sprintf("%s?digest=%s", attackerUploadURL, blobDigest)).
AddBasicAuth(privateUser.Name)
MakeRequest(t, req, http.StatusNotFound)

req = NewRequest(t, "DELETE", attackerUploadURL).
AddBasicAuth(privateUser.Name)
MakeRequest(t, req, http.StatusNotFound)

req = NewRequestWithBody(t, "PATCH", setting.AppURL+uploadURL[1:]+"000", bytes.NewReader(blobContent)).
AddTokenAuth(userToken)
MakeRequest(t, req, http.StatusNotFound)
Expand Down Expand Up @@ -318,7 +336,7 @@ func TestPackageContainer(t *testing.T) {
assert.Equal(t, uploadURL, resp.Header().Get("Location"))
assert.Equal(t, contentRange, resp.Header().Get("Range"))

pbu, err = packages_model.GetBlobUploadByID(t.Context(), uuid)
pbu, err = packages_model.GetBlobUploadByID(t.Context(), uuid, user.ID)
assert.NoError(t, err)
assert.EqualValues(t, len(blobContent), pbu.BytesReceived)

Expand Down
Loading