Summary
Runtipi 4.10.0 allows an authenticated user to upload and restore a crafted backup archive that plants a symlink outside the intended restore boundary. A later normal application write follows that planted symlink and writes attacker-controlled content outside the intended user-config directory.
In my reproduction, I restored a backup containing:
- user-config/app.env -> /data/state/proof.txt
After restore, a normal authenticated request to:
- PUT /api/user-config/demoapp3:_user
caused Runtipi to write attacker-controlled content into:
- /data/state/proof.txt
This provides an authenticated arbitrary file write primitive outside the intended restore and user-config directory boundary.
Root cause
The restore flow accepts archive symlinks from an attacker-controlled backup and copies them into live application paths. Later, FilesystemService.writeTextFile() resolves the requested path lexically but does not reject symlink traversal, so a normal write to user-config/app.env follows the planted symlink target.
PoC
- Authenticate to Runtipi.
- Create or use a custom app, for example demoapp3:_user.
- Prepare a .tar.gz backup containing:
- empty app-data/
- empty app/
- empty user-config/
- symlink member: user-config/app.env -> /data/state/proof.txt
- Upload the archive via
POST /api/backups/demoapp3:_user/upload
- Trigger restore via
POST /api/backups/demoapp3:_user/restore
- Send a normal authenticated
PUT /api/user-config/demoapp3:_user with attacker-controlled appEnv content
- Observe that the content is written to /data/state/proof.txt
Observed result
In my reproduction:
- backup upload succeeded
- restore succeeded
PUT /api/user-config/demoapp3:_user returned HTTP 200
- /data/state/proof.txt was created
- its content matched the attacker-controlled appEnv value:
PWNED_FROM_USERCFG_WRITE
Impact
An authenticated user who can reach the backup restore flow can write attacker-controlled content to arbitrary reachable paths under Runtipi process permissions, provided the target path is usable through a planted symlink in a restored live directory.
Summary
Runtipi 4.10.0 allows an authenticated user to upload and restore a crafted backup archive that plants a symlink outside the intended restore boundary. A later normal application write follows that planted symlink and writes attacker-controlled content outside the intended user-config directory.
In my reproduction, I restored a backup containing:
This provides an authenticated arbitrary file write primitive outside the intended restore and user-config directory boundary.
Root cause
The restore flow accepts archive symlinks from an attacker-controlled backup and copies them into live application paths. Later, FilesystemService.writeTextFile() resolves the requested path lexically but does not reject symlink traversal, so a normal write to user-config/app.env follows the planted symlink target.
PoC
POST /api/backups/demoapp3:_user/uploadPOST /api/backups/demoapp3:_user/restorePUT /api/user-config/demoapp3:_userwith attacker-controlled appEnv contentObserved result
In my reproduction:
PUT /api/user-config/demoapp3:_user returned HTTP 200PWNED_FROM_USERCFG_WRITE
Impact
An authenticated user who can reach the backup restore flow can write attacker-controlled content to arbitrary reachable paths under Runtipi process permissions, provided the target path is usable through a planted symlink in a restored live directory.