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?
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-68The script-library create and test endpoints require only
get_current_user; they do not enforce the documentedsettings.scripts.manage/admin permission or ownership. A caller-suppliedScriptCreate.contentis written as an executable library file, then the test route reads any script and passes its content toexecute_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 atapp/main.py:238The 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 callcheck_repo_access; the download route'sget_current_download_userverifies 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-231The Borgmatic export route depends only on
get_current_userand ignorescurrent_user. With norepository_ids,export_all_repositoriesqueries every repository.export_repositoryplacesrepository.passphraseinto the returned YAML asencryption_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-2885Configuration import requires only
get_current_user. Imported YAML hook fields are copied intoRepository.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, launchesexecute_scheduled_backup_with_maintenance, and callsbackup_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-1852Notification CRUD and test routes require only
get_current_user. Listing queries everyNotificationSettingsrow and the response exposes the rawservice_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 callsApprise.add()followed bynotify(). 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?