WireGuard Portal Vulnerable to Privilege Escalation to Admin via User Self-Update
Description
WireGuard Portal (or wg-portal) is a web-based configuration portal for WireGuard server management. Prior to version 2.1.3, any authenticated non-admin user can become a full administrator by sending a single PUT request to their own user profile endpoint with "IsAdmin": true in the JSON body. After logging out and back in, the session picks up admin privileges from the database. When a user updates their own profile, the server parses the full JSON body into the user model, including the IsAdmin boolean field. A function responsible for preserving calculated or protected attributes pins certain fields to their database values (such as base model data, linked peer count, and authentication data), but it does not do this for IsAdmin. As a result, whatever value the client sends for IsAdmin is written directly to the database. After the exploit, the attacker has full admin access to the WireGuard VPN management portal. The problem was fixed in v2.1.3. The docker images for the tag 'latest' built from the master branch also include the fix.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Authenticated non-admin users can escalate to full admin by sending a PUT request with `IsAdmin: true` to their own profile endpoint, granting full control of the WireGuard VPN management portal.
Vulnerability
Overview
CVE-2026-27899 is a privilege escalation vulnerability in WireGuard Portal (wg-portal), a web-based configuration portal for WireGuard server management. Prior to version 2.1.3, any authenticated non-admin user can become a full administrator by sending a single PUT request to their own user profile endpoint with "IsAdmin": true in the JSON body [1]. After logging out and back in, the session picks up admin privileges from the database [1].
Root
Cause and Exploitation
The vulnerability stems from how the server processes user profile updates. When a user updates their own profile, the server parses the full JSON body into the user model, including the IsAdmin including the boolean field [1]. A function responsible for preserving calculated or protected attributes pins certain fields to their database values (such as base model data, linked peer count, and authentication data), but it does not do this for IsAdmin [1]. As a result, whatever value the client sends for IsAdmin is written directly to the database [1]. The fix, introduced in v2.1.3, adds a validation function that prevents non-admin users from setting IsAdmin` to true [3].
Impact
After successful exploitation, the attacker gains full admin privileges are granted to the attacker, providing full administrative access to the WireGuard VPN management portal [1]. This includes the ability to manage users, peers, and configuration, potentially compromising the entire VPN infrastructure.
Mitigation
The issue is fixed in version 2.1.3 [2]. Docker images tagged 'latest' built from the master branch also include the fix [1]. Users should upgrade to v2.1.3 or later immediately.
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.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/h44z/wg-portalGo | < 2.1.3 | 2.1.3 |
Affected products
2- Range: <2.1.3
- h44z/wg-portalv5Range: < 2.1.3
Patches
13 files changed · +79 −2
internal/app/api/v0/backend/user_service.go+11 −0 modified@@ -53,6 +53,17 @@ func (u UserService) GetAllUsers(ctx context.Context) ([]domain.User, error) { } func (u UserService) UpdateUser(ctx context.Context, user *domain.User) (*domain.User, error) { + sessionUser := domain.GetUserInfo(ctx) + currentUser, err := u.users.GetUser(ctx, user.Identifier) + if err != nil { + return nil, err + } + + // if this endpoint is used by non-admins, make sure that the user can only modify a specific subset of attributes + if !sessionUser.IsAdmin { + user.CopyAdminAttributes(currentUser, u.cfg.Advanced.ApiAdminOnly) + } + return u.users.UpdateUser(ctx, user) }
internal/app/users/user_manager.go+43 −2 modified@@ -352,8 +352,9 @@ func (m Manager) DeactivateApi(ctx context.Context, id domain.UserIdentifier) (* func (m Manager) validateModifications(ctx context.Context, old, new *domain.User) error { currentUser := domain.GetUserInfo(ctx) - if currentUser.Id != new.Identifier && !currentUser.IsAdmin { - return fmt.Errorf("insufficient permissions") + adminErrors := m.validateAdminModifications(ctx, old, new) + if adminErrors != nil { + return adminErrors } if err := old.EditAllowed(new); err != nil && currentUser.Id != domain.SystemAdminContextUserInfo().Id { @@ -387,6 +388,42 @@ func (m Manager) validateModifications(ctx context.Context, old, new *domain.Use return nil } +func (m Manager) validateAdminModifications(ctx context.Context, old, new *domain.User) error { + currentUser := domain.GetUserInfo(ctx) + + if currentUser.IsAdmin { + if currentUser.Id == old.Identifier && !new.IsAdmin { + return fmt.Errorf("cannot remove own admin rights: %w", domain.ErrInvalidData) + } + + return nil // admins can do (almost) everything + } + + // non-admins can only modify very their own profile data + + if currentUser.Id != new.Identifier { + return fmt.Errorf("insufficient permissions: %w", domain.ErrInvalidData) + } + + if new.IsAdmin { + return fmt.Errorf("cannot grant admin rights: %w", domain.ErrInvalidData) + } + + if new.Notes != old.Notes { + return fmt.Errorf("cannot update notes: %w", domain.ErrInvalidData) + } + + if old.Locked != new.Locked || old.LockedReason != new.LockedReason { + return fmt.Errorf("cannot change lock state: %w", domain.ErrInvalidData) + } + + if old.Disabled != new.Disabled || old.DisabledReason != new.DisabledReason { + return fmt.Errorf("cannot change disabled state: %w", domain.ErrInvalidData) + } + + return nil +} + func (m Manager) validateCreation(ctx context.Context, new *domain.User) error { currentUser := domain.GetUserInfo(ctx) @@ -453,6 +490,10 @@ func (m Manager) validateDeletion(ctx context.Context, del *domain.User) error { func (m Manager) validateApiChange(ctx context.Context, user *domain.User) error { currentUser := domain.GetUserInfo(ctx) + if !currentUser.IsAdmin && m.cfg.Advanced.ApiAdminOnly { + return fmt.Errorf("insufficient permissions to change API access: %w", domain.ErrNoPermission) + } + if currentUser.Id != user.Identifier { return fmt.Errorf("cannot change API access of user: %w", domain.ErrNoPermission) }
internal/domain/user.go+25 −0 modified@@ -185,6 +185,31 @@ func (u *User) CopyCalculatedAttributes(src *User) { u.LinkedPeerCount = src.LinkedPeerCount } +// CopyAdminAttributes copies all attributes from the given user except password, passkey and +// api-token if apiAdminOnly is false. +func (u *User) CopyAdminAttributes(src *User, apiAdminOnly bool) { + u.BaseModel = src.BaseModel + u.Identifier = src.Identifier + u.Email = src.Email + u.Source = src.Source + u.ProviderName = src.ProviderName + u.IsAdmin = src.IsAdmin + u.Firstname = src.Firstname + u.Lastname = src.Lastname + u.Phone = src.Phone + u.Department = src.Department + u.Notes = src.Notes + u.Disabled = src.Disabled + u.DisabledReason = src.DisabledReason + u.Locked = src.Locked + u.LockedReason = src.LockedReason + u.LinkedPeerCount = src.LinkedPeerCount + if apiAdminOnly { + u.ApiToken = src.ApiToken + u.ApiTokenCreated = src.ApiTokenCreated + } +} + // DisplayName returns the display name of the user. // The display name is the first and last name, or the email address of the user. // If none of these fields are set, the user identifier is returned.
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-5rmx-256w-8mj9ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-27899ghsaADVISORY
- github.com/h44z/wg-portal/commit/fe4485037a25426446ced95050e9498f477bf71dghsaWEB
- github.com/h44z/wg-portal/releases/tag/v2.1.3ghsaWEB
- github.com/h44z/wg-portal/security/advisories/GHSA-5rmx-256w-8mj9ghsax_refsource_CONFIRMWEB
- hub.docker.com/layers/wgportal/wg-portal/v2.1.3/images/sha256-39acfab55598a74e561828b8cb639515ddc222d6c884996111f5ef235aba9e7bghsaWEB
News mentions
0No linked articles in our index yet.