VYPR
Medium severity4.9OSV Advisory· Published Jul 25, 2025· Updated Apr 15, 2026

CVE-2025-30086

CVE-2025-30086

Description

CNCF Harbor 2.13.x before 2.13.1 and 2.12.x before 2.12.4 allows information disclosure by administrators who can exploit an ORM Leak present in the /api/v2.0/users endpoint to leak users' password hash and salt values. The q URL parameter allows a user to filter users by any column, and filter password=~ could be abused to leak out a user's password hash character by character. An attacker with administrator access could exploit this to leak highly sensitive information stored in the Harbor database. All endpoints that support the q URL parameter are vulnerable to this ORM leak attack.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

CNCF Harbor 2.12.x to 2.13.0 allows admin attackers to leak password hashes via an ORM leak in the /api/v2.0/users endpoint.

Vulnerability

Description

CVE-2025-30086 is an ORM Leak vulnerability in CNCF Harbor versions 2.13.x before 2.13.1 and 2.12.x before 2.12.4. The underlying issue resides in the /api/v2.0/users endpoint, where the q URL parameter allows filtering users by any column. An attacker with administrator access can abuse this by using a filter such as password=~ to leak a user's password hash and salt values character by character. This class of vulnerability, termed ORM Leak, arises when user-supplied input is passed unsanitized to an ORM's filtering methods, allowing an attacker to extract sensitive data through relational filtering attacks [1][2].

Exploitation

Exploitation requires administrator-level access to the Harbor instance. Using the vulnerable q parameter, an admin can perform a character-by-character extraction of password hashes and salts from the database, bypassing any intended access controls on sensitive fields. The vulnerability affects all endpoints that support the q URL parameter, making it a systemic issue within the affected versions [1][2].

Impact

Successful exploitation allows an attacker with admin privileges to obtain highly sensitive credential material (password hashes and salts) for any user. This information could be used offline in brute-force or dictionary attacks to recover plaintext passwords, potentially leading to account takeover and further compromise of the Harbor registry [1][2].

Mitigation

Harbor has fixed this issue in versions 2.13.1 and 2.12.4. Administrators are strongly advised to upgrade to these patched releases or later. No workaround is mentioned in the official advisories. The CVE is not yet listed in CISA's Known Exploited Vulnerabilities catalog [3][4].

AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/goharbor/harborGo
>= 2.13.0, < 2.13.12.13.1
github.com/goharbor/harborGo
>= 2.4.0-rc1.1, < 2.12.42.12.4
github.com/goharbor/harborGo
< 2.4.0-rc1.0.20250331071157-dce7d9f5cffb2.4.0-rc1.0.20250331071157-dce7d9f5cffb

Affected products

2
  • Harbor/HarborOSV2 versions
    v2.12.0, v2.12.0-rc2, v2.12.1, …+ 1 more
    • (no CPE)range: v2.12.0, v2.12.0-rc2, v2.12.1, …
    • (no CPE)range: 2.12.x < 2.12.4, 2.13.x < 2.13.1

Patches

3
dce7d9f5cffb

fix orm filterable issue (#21797)

https://github.com/goharbor/harborWang YanMar 31, 2025via ghsa
7 files changed · +16 9
  • src/common/models/oidc_user.go+1 1 modified
    @@ -23,7 +23,7 @@ type OIDCUser struct {
     	ID     int64 `orm:"pk;auto;column(id)" json:"id"`
     	UserID int   `orm:"column(user_id)" json:"user_id"`
     	// encrypted secret
    -	Secret string `orm:"column(secret)" json:"-"`
    +	Secret string `orm:"column(secret)" filter:"false" json:"-"`
     	// secret in plain text
     	PlainSecret  string    `orm:"-" json:"secret"`
     	SubIss       string    `orm:"column(subiss)" json:"subiss"`
    
  • src/lib/orm/metadata.go+4 1 modified
    @@ -45,7 +45,10 @@ type metadata struct {
     
     func (m *metadata) Filterable(key string) (*key, bool) {
     	k, exist := m.Keys[key]
    -	return k, exist
    +	if !exist {
    +		return nil, false
    +	}
    +	return k, k.Filterable
     }
     
     func (m *metadata) Sortable(key string) bool {
    
  • src/lib/orm/metadata_test.go+4 0 modified
    @@ -66,6 +66,8 @@ func TestParseQueryObject(t *testing.T) {
     	key, exist := metadata.Keys["Field2"]
     	require.True(exist)
     	assert.Equal("Field2", key.Name)
    +	_, filterable := metadata.Filterable("Field2")
    +	assert.False(filterable)
     	assert.False(key.Filterable)
     	assert.True(key.Sortable)
     
    @@ -78,6 +80,8 @@ func TestParseQueryObject(t *testing.T) {
     	key, exist = metadata.Keys["Field3"]
     	require.True(exist)
     	assert.Equal("Field3", key.Name)
    +	_, filterable = metadata.Filterable("Field2")
    +	assert.False(filterable)
     	assert.True(key.Filterable)
     	assert.False(key.Sortable)
     
    
  • src/lib/orm/query.go+1 1 modified
    @@ -162,7 +162,7 @@ func setFilters(ctx context.Context, qs orm.QuerySeter, query *q.Query, meta *me
     			// e.g. "ArtifactDigest" or the snake case format "artifact_digest" should be used instead:
     			// https://github.com/goharbor/harbor/blob/v2.2.0/src/controller/blob/controller.go#L233
     			mk, filterable = meta.Filterable(snakeCase(k))
    -			if !filterable {
    +			if mk == nil || !filterable {
     				continue
     			}
     		}
    
  • src/pkg/reg/dao/model.go+2 2 modified
    @@ -30,8 +30,8 @@ type Registry struct {
     	URL            string    `orm:"column(url)"`
     	Name           string    `orm:"column(name)"`
     	CredentialType string    `orm:"column(credential_type);default(basic)"`
    -	AccessKey      string    `orm:"column(access_key)"`
    -	AccessSecret   string    `orm:"column(access_secret)"`
    +	AccessKey      string    `orm:"column(access_key)" filter:"false"`
    +	AccessSecret   string    `orm:"column(access_secret)" filter:"false"`
     	Type           string    `orm:"column(type)"`
     	Insecure       bool      `orm:"column(insecure)"`
     	Description    string    `orm:"column(description)"`
    
  • src/pkg/robot/model/model.go+2 2 modified
    @@ -32,8 +32,8 @@ type Robot struct {
     	ID           int64     `orm:"pk;auto;column(id)" json:"id"`
     	Name         string    `orm:"column(name)" json:"name" sort:"default"`
     	Description  string    `orm:"column(description)" json:"description"`
    -	Secret       string    `orm:"column(secret)" json:"secret"`
    -	Salt         string    `orm:"column(salt)" json:"-"`
    +	Secret       string    `orm:"column(secret)" filter:"false" json:"secret"`
    +	Salt         string    `orm:"column(salt)" filter:"false" json:"-"`
     	Duration     int64     `orm:"column(duration)" json:"duration"`
     	ProjectID    int64     `orm:"column(project_id)" json:"project_id"`
     	ExpiresAt    int64     `orm:"column(expiresat)" json:"expires_at"`
    
  • src/pkg/user/dao/user.go+2 2 modified
    @@ -32,14 +32,14 @@ type User struct {
     	// Email defined as sql.NullString because sometimes email is missing in LDAP/OIDC auth,
     	// set it to null to avoid unique constraint check
     	Email           sql.NullString `orm:"column(email)" json:"email"`
    -	Password        string         `orm:"column(password)" json:"password"`
    +	Password        string         `orm:"column(password)" filter:"false" json:"password"`
     	PasswordVersion string         `orm:"column(password_version)" json:"password_version"`
     	Realname        string         `orm:"column(realname)" json:"realname"`
     	Comment         string         `orm:"column(comment)" json:"comment"`
     	Deleted         bool           `orm:"column(deleted)" json:"deleted"`
     	SysAdminFlag    bool           `orm:"column(sysadmin_flag)" json:"sysadmin_flag"`
     	ResetUUID       string         `orm:"column(reset_uuid)" json:"reset_uuid"`
    -	Salt            string         `orm:"column(salt)" json:"-"`
    +	Salt            string         `orm:"column(salt)" filter:"false" json:"-"`
     	CreationTime    time.Time      `orm:"column(creation_time);auto_now_add" json:"creation_time"`
     	UpdateTime      time.Time      `orm:"column(update_time);auto_now" json:"update_time"`
     }
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

8

News mentions

0

No linked articles in our index yet.