JumpServer — Authenticated Arbitrary File Read/Write/Delete via Ops Playbook File Browser (Path Traversal → RCE)
| Field | Value |
|---|---|
| Product | JumpServer (Privileged Access Management / bastion host) |
| Vendor repository | https://github.com/jumpserver/jumpserver |
| Vulnerability type | CWE-22 (Improper Limitation of a Pathname to a Restricted Directory / Path Traversal) |
| Component | apps/ops/api/playbook.py — PlaybookFileBrowserAPIView |
| Affected versions | v3.0.0 through v3.6.6 (audited on v3.6.0, commit cb11cb177) |
| Fixed in | v3.7.0 (fix commit f3ca45aa7, introduces django.utils._os.safe_join) — silently, not disclosed as a security fix |
| CVE status | No CVE assigned — this is a distinct issue from CVE-2024-40628/40629 (see "Relationship to CVE-2024-40628/40629" below) |
| Privileges required | Any authenticated low-privilege user (built-in User role) |
| PoC file | poc_vuln001_playbook_traversal_v2.py (also earlier variant poc_path_traversal_001.py) |
The Ops Playbook "file browser" API lets a user read, create, edit, rename and delete files inside the working directory of a playbook they own. The user-controlled key parameter is concatenated onto the playbook working directory with os.path.join() and passed straight to open(), os.rename(), os.remove() and shutil.rmtree() without any path normalization or boundary check. Because os.path.join(base, "../../../etc/passwd") resolves outside base, an attacker can traverse out of the working directory and reach arbitrary paths on the server filesystem.
The four HTTP verbs of the endpoint expose four distinct primitives:
GET→ arbitrary file readPOST→ arbitrary file/directory createPATCH→ arbitrary file write / renameDELETE→ arbitrary file/directory delete
- Confidentiality: read any file the service account can read, including JumpServer's own
config.yml(containsSECRET_KEY, database credentials, Redis credentials),/etc/passwd, etc. - Integrity → RCE: overwrite the
main.ymlof any existing playbook; when that playbook is next executed (by any privileged user or a scheduled job) the attacker-controlled Ansible content runs as theansible-runnerprocess. Writable Python module files can likewise be poisoned. This yields remote code execution from a single low-privilege account. - Availability: delete arbitrary files/directories, causing service disruption or data loss.
A secondary IDOR exists: get_object_or_404(Playbook, id=pk) is not scoped to creator/organization, so a user can also target other users' playbook file browsers. The path traversal alone is already sufficient for full impact.
Base score: 9.9 (Critical)
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
Rationale: network-reachable authenticated API (AV:N, PR:L), no user interaction, and the write→RCE primitive breaks out of the application's file sandbox to affect the underlying OS/other tenants (Scope changed), with full C/I/A loss.
- Attacker holds a valid session for any account with the built-in
Userrole. That role is granted("ops", "playbook", "*", "*")by default (apps/rbac/builtin.py:22), which includesops.add_playbookandops.change_playbook. - The JumpServer web API is reachable.
- Authenticate as a normal user and obtain a session + CSRF token.
- Create an owned playbook:
POST /api/v1/ops/playbooks/with{"name":"poc","create_method":"blank"}→ returns the playbook UUID. - Arbitrary read (leak secrets):
GET /api/v1/ops/playbook/<uuid>/file/?key=../../../../config.yml - Arbitrary write → RCE:
PATCH /api/v1/ops/playbook/<uuid>/file/with body{"key":"../../<path-to-another-playbook>/main.yml","content":"<malicious ansible/jinja>"}. The payload executes when that playbook is next run. - Arbitrary delete:
DELETE /api/v1/ops/playbook/<uuid>/file/?key=../../../../<target>
# GET — line 82-91 (arbitrary read)
def get(self, request, **kwargs):
playbook = get_object_or_404(Playbook, id=kwargs.get('pk'))
work_path = playbook.work_dir
file_key = request.query_params.get('key', '')
if file_key:
file_path = os.path.join(work_path, file_key) # <-- no normalization / boundary check
with open(file_path, 'r') as f:
content = f.read()
return Response({'content': content})
# PATCH — line 169 & 183-186 (arbitrary write / rename)
file_path = os.path.join(work_path, file_key) # <-- traversal
...
if new_name:
new_file_path = os.path.join(os.path.dirname(file_path), new_name)
os.rename(file_path, new_file_path)
else:
with open(file_path, 'w') as f: # <-- arbitrary write
f.write(content)
# DELETE — line 195-199 (arbitrary delete)
file_path = os.path.join(work_path, file_key)
if os.path.isdir(file_path):
shutil.rmtree(file_path) # <-- arbitrary rmtree
else:
os.remove(file_path)Note: the startswith('.') checks elsewhere in the class only build front-end tree node display IDs — they are not a security control.
- User-controlled
key—request.query_params.get('key')(GET/DELETE) andrequest.data.get('key')(POST/PATCH). work_dirbase —apps/ops/models/playbook.py:77-79:@property def work_dir(self): work_dir = os.path.join(settings.DATA_DIR, "ops", "playbook", self.id.__str__()) return work_dir
- Default privilege grant —
apps/rbac/builtin.py:22:user_perms = ( ... ("ops", "playbook", "*", "*"), # built-in "User" role → add/change playbook )
HTTP GET/POST/PATCH/DELETE /api/v1/ops/playbook/<uuid:pk>/file/
└─ PlaybookFileBrowserAPIView.get()/post()/patch()/delete() (apps/ops/api/playbook.py)
└─ work_path = Playbook.work_dir (apps/ops/models/playbook.py:77)
└─ file_path = os.path.join(work_path, file_key) # user-controlled key, no sanitization
└─ open(file_path) / open(file_path,'w') / os.rename / os.remove / shutil.rmtree
This vulnerability is not covered by the existing JumpServer Ansible-playbook CVEs (CVE-2024-40628 arbitrary file read, GHSA-rpf7-g4xh-84v9; CVE-2024-40629 arbitrary file write → RCE, GHSA-3wgp-q8m7-v33v). They are distinct issues in different code paths, proven by patch analysis:
-
Different component / attack surface. CVE-2024-40628/40629 are triggered through the Job Center "Job > Template / Job list" flow, abusing Ansible execution-time behavior (the lookup plugin / writing into Ansible package directories inside the Celery container). This report's vulnerability is a direct path traversal in the REST file-browser endpoint
PlaybookFileBrowserAPIView(/api/v1/ops/playbook/<uuid>/file/), independent of any playbook execution. -
Different fix, different release. The CVE-2024-40628/40629 fix shipped in v3.10.12 (July 2024) and did not modify
apps/ops/api/playbook.pyat all — verified: no commit in thev3.10.11..v3.10.12range touches that file; the actual fix reworked the Ansible runner ("isolated mode", commit165d030c8,apps/ops/ansible/callback.py/runner.py). The file-browser path traversal was instead fixed much earlier, in v3.7.0, by commitf3ca45aa7dated 2023-09-19, whose message isperf: 优化 Playbook 文件创建逻辑— a refactor with no security wording and no associated CVE/advisory (a silent fix). -
Version ranges are mutually exclusive. CVE-2024-40629 lists v3.0.0–v3.10.11 as affected. Yet by v3.10.11 the file browser already used
safe_joinin all handlers (introduced in v3.7.0), i.e. it was no longer vulnerable throughout most of that CVE's stated range. A single bug cannot be "still vulnerable at v3.10.11" (the CVE) and "already fixed since v3.7.0" (this endpoint) simultaneously — confirming they are separate vulnerabilities.
Conclusion: the PlaybookFileBrowserAPIView path traversal is an independent, previously undisclosed vulnerability affecting v3.0.0–v3.6.6, silently fixed in v3.7.0, with no CVE currently assigned — eligible for a new CVE request.
Fixed in v3.7.0 by replacing all os.path.join(work_path, file_key) calls with django.utils._os.safe_join, which raises SuspiciousFileOperation when the resolved path escapes the base directory. The IDOR should additionally be closed by scoping get_object_or_404 to creator/organization.