Skip to content

Borg UI: Authorization gaps enable RCE, cross-repo access, passphrase disclosure and SSRF

Critical
karanhudia published GHSA-qmvm-37wj-2v72 Sep 28, 2026

Software

borg-ui

Affected versions

< 2.3.4

Patched versions

2.3.4

Description

Security Disclosure: Borg UI — authorization gaps enable RCE, cross-repo data access, passphrase disclosure, and SSRF (5 HIGH findings)

Summary

Borg UI (http://github.com/karanhudia/borg-ui), a FastAPI web UI for Borg backup, exposes multiple unauthenticated-by-permission endpoints. All findings were verified by static source review at commit 0309d713eb5c000e934d654d4c8e6f0c8b07cc5b (version 0.1.3). No runtime exploitation was performed.

Finding 1 — Authenticated non-admin users can execute arbitrary script-library shell code (CWE-862 / CWE-78, HIGH)

Location: app/api/scripts_library.py:387-421,782-896; app/services/script_executor.py:18-68

The script-library create and test endpoints require only get_current_user; they do not enforce the documented settings.scripts.manage/admin permission or ownership. A caller-supplied ScriptCreate.content is written as an executable library file, then the test route reads any script and passes its content to execute_script. The shared executor writes the content to a temporary shell file and starts /bin/bash. Arbitrary shell commands execute in the Borg UI server process context.

Finding 2 — Borg 2 archive reads and downloads omit repository authorization (CWE-639 / CWE-862, HIGH)

Location: app/api/v2/archives.py:136-165,240-247,404-417,600-610; registered at app/main.py:238

The Borg 2 archive helper resolves a repository from the caller-supplied numeric ID or path and checks only borg_version == 2. The list, archive-info, contents, and download routes authenticate a user but never call check_repo_access; the download route's get_current_download_user verifies identity/token only. A low-privilege user can read metadata/file listings and request extraction of files from another Borg 2 repository, crossing repository boundaries.

Finding 3 — Any authenticated user can export all repository passphrases (CWE-862 / CWE-200, HIGH)

Location: app/routers/config.py:36-69; app/services/borgmatic_service.py:107-149,177-203; app/database/models.py:220-231

The Borgmatic export route depends only on get_current_user and ignores current_user. With no repository_ids, export_all_repositories queries every repository. export_repository places repository.passphrase into the returned YAML as encryption_passphrase; the model stores this value in a regular string column. The project documentation states that export/import management requires the admin role. A normal viewer/operator can download the passphrases and paths for all configured repositories.

Finding 4 — Any authenticated user can import executable backup hooks and create an enabled schedule (CWE-862 / CWE-78, HIGH)

Location: app/routers/config.py:72-81; app/services/borgmatic_service.py:654-724,810-845,891-935; app/api/schedule.py:2572-2577,2823-2885

Configuration import requires only get_current_user. Imported YAML hook fields are copied into Repository.pre_backup_script/post_backup_script. When retention data is supplied, import creates a schedule for the new repository, sets its repository path, calculates a next run, and defaults it enabled. The scheduler resolves that path, launches execute_scheduled_backup_with_maintenance, and calls backup_service.execute_backup; the backup path executes the repository's inline hooks through the shell executor. A low-privilege user can persist arbitrary server-side backup-hook commands that execute during the resulting scheduled backup.

Finding 5 — Any authenticated user can read and mutate global notification endpoints (CWE-862 / CWE-200 / CWE-918, HIGH)

Location: app/api/notifications.py:93-135,160-264; app/database/models.py:1708-1765; app/services/notification_service.py:87-120,1741-1805,1830-1852

Notification CRUD and test routes require only get_current_user. Listing queries every NotificationSettings row and the response exposes the raw service_url; the model is global and has no owner. Create/update/delete can alter global delivery settings. The test endpoint accepts an arbitrary Apprise URL and calls Apprise.add() followed by notify(). Event delivery similarly constructs Apprise from each stored service URL and sends repository/backup/restore status content. A low-privilege user can disclose notification URLs containing service credentials, delete or redirect global alerts, and cause the server to make outbound Apprise requests to a caller-selected destination.

Responsible disclosure

Coordinated-disclosure practice: no exploitation against live systems, no public disclosure, no attack testing beyond source analysis. Report provided for coordinated remediation. Thank you for maintaining Borg UI.

Summary

Short summary of the problem. Make the impact and severity as clear as possible. For example: An unsafe deserialization vulnerability allows any unauthenticated user to execute arbitrary code on the server.

Details

Give all details on the vulnerability. Pointing to the incriminated source code is very helpful for the maintainer.

PoC

Complete instructions, including specific configuration details, to reproduce the vulnerability.

Impact

What kind of vulnerability is it? Who is impacted?

Severity

Critical

CVE ID

No known CVE

Weaknesses

Missing Authorization

The product does not perform an authorization check when an actor attempts to access a resource or perform an action. Learn more on MITRE.

Credits