mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-20 10:27:54 +00:00
Backport #38406 by @bircni Addresses a batch of privately reported security issues, grouped by area: - **SSRF** - migration PR-patch/asset fetches, OAuth2 avatar & OpenID discovery, pull-mirror URL re-validation, and the outbound proxy path. - **Access-token scope** - prevent scope escalation on token creation; keep public-only tokens confined (feeds, packages, Actions listings, star/watch lists, limited/private owners). - **Access control / disclosure** - go-get default-branch leak, webhook authorization-header leak, watch clearing on private transitions, label/attachment scoping. - **Denial of service** - input bounds for npm dist-tags, Debian control files, Arch file lists, and SSH keys. ### 📌 Attention for site admins Not breaking - existing configs keep working - but two changes are worth a look: - **New SSRF protection** Outbound requests (migrations, OAuth2 avatars, OpenID discovery, pull mirrors, proxy path) are now validated against the allow/block host lists. If your instance legitimately reaches internal hosts, you may need to add them to `[security].ALLOWED_HOST_LIST` (and the relevant `ALLOW_LOCALNETWORKS` settings). - **Deprecation** `[webhook].ALLOWED_HOST_LIST` is deprecated and will be removed in a future release. Use `[security].ALLOWED_HOST_LIST` instead; the old key still works for now. Co-authored-by: bircni <bircni@icloud.com> Co-authored-by: TheFox0x7 <thefox0x7@gmail.com> Co-authored-by: techknowlogick <techknowlogick@gitea.io> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: Zettat123 <zettat123@gmail.com>
This commit is contained in:
@@ -73,7 +73,9 @@ func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) {
|
||||
}
|
||||
|
||||
if publicOnly {
|
||||
if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() {
|
||||
// a public-only token must not reach limited-visibility owners either,
|
||||
// matching how orgs/users are enforced elsewhere in this file
|
||||
if ctx.Package != nil && !ctx.Package.Owner.Visibility.IsPublic() {
|
||||
ctx.HTTPError(http.StatusForbidden, "reqToken", "token scope is limited to public packages")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -333,11 +333,18 @@ func ListPackageTags(ctx *context.Context) {
|
||||
func AddPackageTag(ctx *context.Context) {
|
||||
packageName := packageNameFromParams(ctx)
|
||||
|
||||
body, err := io.ReadAll(ctx.Req.Body)
|
||||
// the dist-tag body is only a quoted version string; bound it to avoid an unbounded
|
||||
// read that could exhaust memory
|
||||
const maxDistTagBodySize = 4 * 1024
|
||||
body, err := io.ReadAll(io.LimitReader(ctx.Req.Body, maxDistTagBodySize+1))
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
if len(body) > maxDistTagBodySize {
|
||||
apiError(ctx, http.StatusRequestEntityTooLarge, errors.New("request body too large"))
|
||||
return
|
||||
}
|
||||
version := strings.Trim(string(body), "\"") // is as "version" in the body
|
||||
|
||||
pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeNpm, packageName, version)
|
||||
|
||||
@@ -291,7 +291,9 @@ func checkTokenPublicOnly() func(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
case auth_model.AccessTokenScopeCategoryPackage:
|
||||
if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() {
|
||||
// a public-only token must not reach limited-visibility owners either,
|
||||
// matching the org/user public-only enforcement above
|
||||
if ctx.Package != nil && !ctx.Package.Owner.Visibility.IsPublic() {
|
||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public packages")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -178,13 +178,12 @@ func DeleteIssueLabel(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
label, err := issues_model.GetLabelByID(ctx, ctx.PathParamInt64("id"))
|
||||
// the label must belong to this repo (or its owning org); otherwise a foreign label ID
|
||||
// is rejected the same way as a nonexistent one, closing a cross-repo enumeration oracle
|
||||
labelID := ctx.PathParamInt64("id")
|
||||
label, err := issues_model.GetLabelInRepoOrOrgByID(ctx, ctx.Repo.Repository.ID, ctx.Repo.Owner.ID, ctx.Repo.Owner.IsOrganization(), labelID)
|
||||
if err != nil {
|
||||
if issues_model.IsErrLabelNotExist(err) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -221,7 +221,8 @@ func AddTime(ctx *context.APIContext) {
|
||||
// allow only RepoAdmin, Admin and User to add time
|
||||
user, err = user_model.GetUserByName(ctx, form.User)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,11 +106,7 @@ func GetLabel(ctx *context.APIContext) {
|
||||
l, err = issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, intID)
|
||||
}
|
||||
if err != nil {
|
||||
if issues_model.IsErrRepoLabelNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -214,11 +210,7 @@ func EditLabel(ctx *context.APIContext) {
|
||||
form := web.GetForm(ctx).(*api.EditLabelOption)
|
||||
l, err := issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
if issues_model.IsErrRepoLabelNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -1251,6 +1251,13 @@ func UpdatePullRequest(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
// a public-only token must not update (push into) a private head repo,
|
||||
// even when the base repo named in the route is public
|
||||
if !ctx.TokenCanAccessRepo(pr.HeadRepo) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
// keep API back-compat: when no style is given, default to "merge" rather than the repo's DefaultUpdateStyle,
|
||||
// so existing API clients keep getting a merge update.
|
||||
rebase := repo_model.UpdateStyle(ctx.FormString("style", string(repo_model.UpdateStyleMerge))) == repo_model.UpdateStyleRebase
|
||||
|
||||
@@ -21,8 +21,21 @@ import (
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// actionsOwnerAccessibleRepoIDsSubQuery returns the sub-query restricting an owner-scoped actions
|
||||
// listing to the repos whose actions the caller can read, or nil when no restriction applies. A bare
|
||||
// org member must not be able to enumerate runs/jobs of repos they have no access to. A site admin may
|
||||
// skip the access filter, but a public-only token must stay confined to public repos even for an admin.
|
||||
func actionsOwnerAccessibleRepoIDsSubQuery(ctx *context.APIContext, ownerID int64) *builder.Builder {
|
||||
if ownerID > 0 && (ctx.Doer == nil || !ctx.Doer.IsAdmin || ctx.PublicOnly) {
|
||||
return repo_model.FindUserActionsAccessibleOwnerRepoIDsSubQuery(ownerID, ctx.Doer, ctx.PublicOnly)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListJobs lists jobs for api route validated ownerID and repoID
|
||||
// ownerID == 0 and repoID == 0 means all jobs
|
||||
// ownerID == 0 and repoID != 0 means all jobs for the given repo
|
||||
@@ -60,6 +73,8 @@ func ListJobs(ctx *context.APIContext, ownerID, repoID, runID int64, runAttemptI
|
||||
opts.Statuses = append(opts.Statuses, values...)
|
||||
}
|
||||
|
||||
opts.AccessibleRepoIDsSubQuery = actionsOwnerAccessibleRepoIDsSubQuery(ctx, opts.OwnerID)
|
||||
|
||||
jobs, total, err := db.FindAndCount[actions_model.ActionRunJob](ctx, opts)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
@@ -181,6 +196,8 @@ func ListRuns(ctx *context.APIContext, ownerID, repoID int64, workflowID string)
|
||||
}
|
||||
excludePullRequests := ctx.FormBool("exclude_pull_requests")
|
||||
|
||||
opts.AccessibleRepoIDsSubQuery = actionsOwnerAccessibleRepoIDsSubQuery(ctx, opts.OwnerID)
|
||||
|
||||
runs, total, err := db.FindAndCount[actions_model.ActionRun](ctx, opts)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
|
||||
@@ -126,6 +126,29 @@ func CreateAccessToken(ctx *context.APIContext) {
|
||||
}
|
||||
t.Scope = scope
|
||||
|
||||
// a token-authenticated request must not mint a token with a broader scope than its own
|
||||
if ctx.Data["IsApiToken"] == true {
|
||||
apiTokenScope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
|
||||
if !ok {
|
||||
ctx.APIError(http.StatusForbidden, "the authenticating token has no scope")
|
||||
return
|
||||
}
|
||||
hasScope, err := apiTokenScope.CanCreateChildScope(scope)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if !hasScope {
|
||||
ctx.APIError(http.StatusForbidden, "cannot create an access token with a broader scope than the authenticating token")
|
||||
return
|
||||
}
|
||||
// a public-only token must not mint a token that drops the public-only restriction
|
||||
if t.Scope, err = t.Scope.EnforcePublicOnlyFrom(apiTokenScope); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := auth_model.NewAccessToken(ctx, t); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/context"
|
||||
@@ -57,6 +58,11 @@ func AddEmail(ctx *context.APIContext) {
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) {
|
||||
ctx.APIErrorNotFound("emails are not allowed to be changed")
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*api.CreateEmailOption)
|
||||
if len(form.Emails) == 0 {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "Email list empty")
|
||||
@@ -114,6 +120,11 @@ func DeleteEmail(ctx *context.APIContext) {
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) {
|
||||
ctx.APIErrorNotFound("emails are not allowed to be changed")
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*api.DeleteEmailOption)
|
||||
if len(form.Emails) == 0 {
|
||||
ctx.Status(http.StatusNoContent)
|
||||
|
||||
Reference in New Issue
Block a user