
Stirling-PDF v3.2.0 - High - Remote - IDOR - processing-folder storage root lists and overwrites peer PDFs
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · stirling-storage-folder-idor
Stirling-PDF 3.2.0 - Stirling Tools
Class: IDOR. Reach: Remote ROLE_USER.
POST /api/v1/processing-folders has no @PreAuthorize beyond a logged-in user. FolderAccessGuard implies the whole local storage basePath (./storage) even when policies.allowedFolderRoots is empty. LocalStorageProvider writes {ownerId}/{uuid}_{filename}. A ROLE_USER can set directory: "storage/<victimUserId>", list that owner's PDFs, and sweep them in place with replace=true.
Direct /api/v1/storage/files/{id} is patched (findByIdAndOwner). Lab GET of the victim file as the peer is 403. The leftover is the processing-folder implied root, not the storage CRUD API.
A ROLE_USER lists another user's stored PDF names and overwrites those blobs. The victim download returns the tampered file.
| ID | no CVE yet |
| CWE | CWE-639 / CWE-73 |
| CVSS | 7.1 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:N |
| Product | Stirling-PDF |
| Affected | v3.2.0 (a7e593728dd9aa81e75318a949789d3c2e9ee754), image stirlingtools/stirling-pdf:3.2.0-fat, login-on, storage.enabled=true |
| Auth | ROLE_USER on the default team |
| License | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1:18154 only |
Leftover of the Stirling-PDF v3.2.0 leftovers series. Sibling: stirling-h2-import-leftover (H2 CREATE OR REPLACE ALIAS). This pack is the storage IDOR.
Any logged-in human ROLE_USER on login-on proprietary/fat with local storage enabled (template default). Official compose is login-off plus DISABLE_ADDITIONAL_FEATURES=true; that deploy 403s /api/v1/storage and omits this controller. The HIT deploy is template login-on plus fat.
Privilege table (exhaustive). Same bar as Signal-Server Dropwizard admin.
| Axis | Method + path | Grants | Provenance |
|---|---|---|---|
| Recon | ROLE_USER GET /api/v1/user/users | Numeric userId for admin, peer, and STIRLING-PDF-BACKEND-API-USER | Lab-proven |
| Recon | POST /api/v1/processing-folders {directory:"storage/1"} then GET /api/v1/processing-folders/{id}/files | Name, size, mtime of admin's stored PDF (5341de35-8729-49cf-92d0-c8284e05bfc5_victim-lab.pdf) | Lab-proven |
| Integrity | POST /api/v1/processing-folders/{id}/sweep with /api/v1/security/add-watermark then victim Range GET /api/v1/storage/files/1/download | In-place rewrite. Victim 206 / 3450 bytes contains STIRLING-STORAGE-FOLDER-IDOR-WITNESS | Lab-proven (replace=true) |
| Confidentiality | same sweep plus a team outputId / outbound step the attacker can read | Copy of victim PDF bytes | Inferred. Not required for listing/overwrite |
Stays closed on this pin
| Axis | Method + path | Why it is closed |
|---|---|---|
| Recon | GET /api/v1/storage/files/{id} of a non-shared file | 403 Access denied. Lab-proven on file id 1 |
| Recon | unauth /api/v1/storage/** | 403 when login is off (ensureStorageEnabled) |
| Integrity | folder UUID CRUD across owners | findByIdAndOwner |
| Integrity | LocalStorageProvider .. / absolute key | Path.startsWith(basePath) after normalize |
| Integrity | ultra-lite image | proprietary processing-folder controller absent |
GET /api/v1/user/users listing STIRLING-PDF-BACKEND-API-USER is extra IOC. I did not file it as its own leftover. Un-ranged victim download 500s after overwrite because FileStorageController still advertises the original DB sizeBytes. Range works. Watermark text is Identity-H in FlateDecode; the oracle inflates streams.
3.x added local storage and processing folders. Storage CRUD is owner-scoped. FolderAccessGuard.serverStorageRoots returns Path.of(basePath) for the entire tree, not basePath/{currentUserId}. storage/<victimId> startsWith that implied root.
The lab creates peeruser as ROLE_USER, uploads a victim PDF as admin, proves direct storage GET 403, points a processing folder at storage/1, lists the UUID-prefixed name, sweeps a watermark, then Range-downloads as the victim.
Wrong turns: treating direct storage IDOR as still open (403); treating login-off official compose as the HIT deploy (storage 403, controller absent); claiming attacker-readable byte copy without an outputId.
python3 stirling-storage-folder-idor-Abraxas-Labs.py
or
cd lab
./run.sh
Target only http://127.0.0.1:18154. Image stirlingtools/stirling-pdf:3.2.0-fat. Docker required. Login-on. The launcher chdirs into lab/ and execs run.sh.
IOC direct-storage-get http=403
IOC processing-folders-save directory=storage/1 http=202
IOC listed-peer ... victim-lab.pdf
IOC victim-download attempt=1 http=206 bytes=3450 witness=1
SUCCESS stirling-storage-folder-idor direct-storage-403=yes listed-peer-pdf=yes download-has-witness=yes STIRLING-STORAGE-FOLDER-IDOR-WITNESS
Scope the implied storage root to basePath/{currentUserId} for disk directory on multi-user servers. Refuse storage/<otherId> the way findByIdAndOwner already refuses foreign file ids. Recompute advertised sizeBytes after an in-place rewrite.
Re-run the loopback client against a patched build: directory=storage/1 as peeruser must 403, and listed-peer must stay empty.
Until then, do not treat "I patched /api/v1/storage/files/{id}" as "I scoped every API that takes a disk path under ./storage."
a7e593728dd9aa81e75318a949789d3c2e9ee754)Vendor intake: GitHub Security Advisory tab or [email protected]. I am not opening a public issue on Stirling-Tools/Stirling-PDF.
Abraxas Labs https://abraxaslabs.tech https://github.com/abraxas [email protected]