Feb 27, 2026·6 min read·13 visits
WireGuard Portal trusted user input too much. By sending `"IsAdmin": true` in a profile update, any standard user becomes a root-level administrator. Fixed in v2.1.3 by explicitly filtering sensitive fields.
A critical Privilege Escalation vulnerability in h44z/wg-portal allows any authenticated user to promote themselves to Administrator by simply adding a JSON field to a profile update request. This classic Mass Assignment vulnerability exposes the entire VPN management interface to compromise.
WireGuard is the darling of the VPN world—lean, fast, and cryptographically sound. But nobody likes managing config files via CLI, so tools like wg-portal exist to give us a nice, shiny web UI. It handles user management, peer provisioning, and the delicate dance of public keys. It is the gatekeeper to your private network.
Now, imagine that gatekeeper has a clipboard. When you walk up to update your profile (say, to change your last name), the gatekeeper hands you the clipboard and says, "Here, write down your new details." You write your name, and then, at the bottom of the page, you scribble in a new box: "Is Boss: YES."
In a secure system, the gatekeeper would look at that and say, "Nice try, buddy." In wg-portal versions prior to 2.1.3, the gatekeeper just nods, types it into the database, and hands you the keys to the castle. This is CVE-2026-27899, a classic, textbook Mass Assignment vulnerability that turns a lowly user into a network god with a single HTTP request.
The root cause here is a failure in the Object-Relational Mapping (ORM) or, more specifically, the JSON unmarshalling logic. In modern web development, we love convenience. We love taking a JSON blob from a frontend request and magically mapping it to a backend struct. It saves us from writing boring code like user.Name = request.Name fifty times.
However, this convenience is a loaded gun. The wg-portal application defined a User struct that represented the database schema. This struct contained everything about a user: their email, their name, their linked peers, and—crucially—their privileges, represented by the IsAdmin boolean.
When the PUT /api/v1/user/profile endpoint received a request, it didn't use a Data Transfer Object (DTO) or a whitelist of allowed fields. It took the raw JSON and unmarshalled it directly into the User domain model. Because the IsAdmin field was public (exported in Go) and tagged for JSON, the standard library happily overwrote the existing false value with whatever the attacker provided. The application failed to ask the most important question: "Is this user actually allowed to change this specific field?"
Let's look at the anatomy of the failure. In Go, if you have a struct like this:
type User struct {
Identifier string `json:"id"`
Email string `json:"email"`
IsAdmin bool `json:"isAdmin"` // <--- The dangerous field
// ... other fields
}And you handle updates like this:
func UpdateProfile(c echo.Context) error {
u := new(User)
if err := c.Bind(u); err != nil { // <--- Auto-binds ALL JSON fields
return err
}
// Save u to database...
}You have created a security hole. The fix, introduced in version 2.1.3 (specifically commit fe4485037a25426446ced95050e9498f477bf71d), was to stop trusting the binding process blindly. The developer introduced a method called CopyAdminAttributes.
Instead of taking the user's input as gospel, the system now fetches the existing user from the database. If the user making the request is NOT an admin, the system forcibly overwrites the sensitive fields in the input object with the values from the database, effectively ignoring the attacker's attempt to toggle the boolean.
// The Fix Logic (Simplified)
func (u *User) CopyAdminAttributes(src *User, apiAdminOnly bool) {
// ...
if !apiAdminOnly {
u.IsAdmin = src.IsAdmin // Reset to DB value, ignoring input
u.Locked = src.Locked
u.Disabled = src.Disabled
}
}They also added validateAdminModifications to throw an explicit error, but the CopyAdminAttributes function is the real shield here. It ensures that even if you send the data, it gets sanitized before persistence.
Exploiting this is almost insultingly easy. You don't need buffer overflows or heap spraying. You just need Burp Suite, Postman, or curl.
Step 1: Authenticate Log in as a standard user. Get your session cookie or JWT.
Step 2: Capture the Update Request Go to your profile page and change something trivial, like your name. Capture that request. It will look something like this:
PUT /api/v1/user/profile HTTP/1.1
Host: vpn.target.com
Content-Type: application/json
Cookie: session=...
{
"firstname": "John",
"lastname": "Doe",
"email": "john@example.com"
}Step 3: The Injection Modify the JSON body to include the forbidden flag. You don't even need to be subtle.
{
"firstname": "John",
"lastname": "Doe",
"email": "john@example.com",
"IsAdmin": true
}Step 4: Execute
Send the request. The server responds with 200 OK. The database is updated.
Step 5: Enjoy the Power Refresh the page or log out and back in. The UI will render the "Administration" tab. You can now revoke the CEO's VPN keys, create backdoors for yourself, or delete the entire user base.
Why is this a CVSS 8.8 and not higher? Only because you need a valid account first. But in a corporate environment, "valid account" just means "any employee" or "anyone who phished an employee."
Once an attacker becomes an Admin in wg-portal, the game is over. They gain:
This isn't just about changing a flag in a database; it's about bypassing the perimeter security of the organization.
If you are running wg-portal < 2.1.3, you are vulnerable. The remediation is straightforward: Update immediately.
docker pull h44z/wg-portal:latest (or specifically tag v2.1.3).For Developers: This vulnerability is a harsh lesson in API design. Never bind your internal database models directly to your public API endpoints. Always use DTOs (Data Transfer Objects).
Create a UpdateUserRequest struct that only contains the fields a user is allowed to change (FirstName, LastName). If IsAdmin isn't in that struct, the JSON parser physically cannot overwrite it, no matter what the attacker sends. Explicit whitelisting is always safer than blacklisting or manual sanitization.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
wg-portal h44z | < 2.1.3 | 2.1.3 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-269 (Improper Privilege Management) |
| Attack Vector | Network (API) |
| CVSS v3.1 | 8.8 (High) |
| Exploit Status | Proof-of-Concept (Trivial) |
| Patch Date | 2026-02-23 |
| Impact | Full Administrative Access |
The application does not properly assign, modify, track, or check privileges for an actor, creating an unintended sphere of control for that actor.