CyberPanel Backup IDOR
CVE-2026-65917: CyberPanel's incremental-backup handlers let authenticated users hijack or delete any tenant's backups via sequential IDs.
Overview
CyberPanel, a widely-deployed open-source web hosting control panel built on top of OpenLiteSpeed, contains an Insecure Direct Object Reference (IDOR) vulnerability in its IncBackups application that affects all versions through 1.9.1. The flaw resides in three incremental-backup request handlers — deleteBackup, fetchRestorePoints, and restorePoint — each of which accepts a caller-supplied IncJob integer ID and performs sensitive backup operations without first verifying that the referenced job belongs to a domain owned by the authenticated user. Because IncJob primary keys are globally sequential integers assigned by the database, any authenticated panel user can enumerate the ID space and interact with backup jobs created by other tenants on the same server.
The vulnerability was identified through analysis of the project’s source code and reported via the CyberPanel issue tracker. It was silently fixed in commit b198460 without a coordinated disclosure advisory or CVE assignment at the time of patch, meaning operators who follow the project’s release notes rather than individual commits may have remained unaware of the exposure. CyberPanel is commonly deployed in shared-hosting and managed VPS environments where strict tenant isolation is a hard requirement, making this class of flaw particularly damaging.
Any user who can log in to the CyberPanel web interface — including resellers and low-privilege accounts — is a potential attacker. No special role or elevated permission is required beyond basic authentication, contributing to the CVSS 8.8 HIGH rating.
Technical Analysis
The root cause is straightforward but consequential: the IncBackups view functions retrieve an IncJob database record using a raw integer supplied by the client and never assert that incJob.domain is among the domains the requesting user is authorized to manage.
The following simplified reconstruction illustrates the vulnerable pattern present before the patch:
# IncBackups/views.py (pre-patch, illustrative reconstruction)
from django.http import JsonResponse
from .models import IncJob
def deleteBackup(request):
"""Delete an incremental backup snapshot."""
data = json.loads(request.body)
job_id = data.get('jobID') # ① Attacker-controlled integer
try:
inc_job = IncJob.objects.get(id=job_id) # ② Fetched by raw PK — no owner check
except IncJob.DoesNotExist:
return JsonResponse({'error': 'Job not found'}, status=404)
# ③ Proceeds directly to destructive operation on another tenant's data
result = remove_backup_snapshot(inc_job)
return JsonResponse({'status': 'deleted', 'job': job_id})
def fetchRestorePoints(request):
data = json.loads(request.body)
job_id = data.get('jobID')
inc_job = IncJob.objects.get(id=job_id) # Same pattern — no authorization gate
points = list_restore_points(inc_job)
return JsonResponse({'restorePoints': points})
def restorePoint(request):
data = json.loads(request.body)
job_id = data.get('jobID')
inc_job = IncJob.objects.get(id=job_id) # Same pattern
# Restoration runs with root-level filesystem privileges
trigger_restore(inc_job)
return JsonResponse({'status': 'restore initiated'})
Three distinct failure modes compound each other here. First, IncJob.objects.get(id=job_id) performs a lookup exclusively on the database primary key, which is a globally auto-incremented integer. Second, no authorization predicate — such as filtering by the domains associated with request.user — is applied before the record is acted upon. Third, because IncJob IDs are sequential, an attacker does not need to guess or brute-force opaque identifiers; simple linear enumeration starting from 1 is sufficient to discover valid jobs across the entire server population.
The restorePoint handler carries the highest severity because CyberPanel’s backup restoration subsystem executes with root privileges to restore file ownership and permissions. An attacker can therefore not only read another tenant’s backup metadata (fetchRestorePoints) or permanently destroy their snapshots (deleteBackup), but also trigger a root-privileged process that overwrites live filesystem state with data from a foreign backup job (restorePoint).
Impact
The practical blast radius of this vulnerability in a shared-hosting context is significant. An attacker with any valid CyberPanel login can:
- Read confidential backup metadata — file lists, directory trees, snapshot timestamps, and storage paths belonging to any other tenant on the server via
fetchRestorePoints. - Irreversibly delete another tenant’s backup history —
deleteBackuphas no recycle-bin or soft-delete semantic; snapshots are permanently removed, with direct implications for that tenant’s disaster-recovery posture. - Trigger unauthorized root-privileged restoration — invoking
restorePointagainst a foreign job can overwrite a victim tenant’s live website files and databases with arbitrary backup content, constituting both data destruction and a potential privilege-escalation primitive if the restored content includes attacker-controlled web shells or configuration files.
From a CVSS 3.1 perspective, the 8.8 score reflects: network-accessible attack vector (AV:N), low attack complexity (AC:L), low privileges required (PR:L), no user interaction (UI:N), and high impact across confidentiality, integrity, and availability (C:H/I:H/A:H). The sole mitigating factor is the requirement for prior authentication.
How to Fix It
The fix applied in commit b198460 follows the correct pattern: after retrieving the IncJob record, the handler validates that the job’s associated domain is within the set of domains the authenticated user owns before proceeding.
# IncBackups/views.py (post-patch pattern)
from django.http import JsonResponse
from .models import IncJob
from loginSystem.models import Administrator
def _get_authorized_job(job_id, request_user):
"""
Retrieve an IncJob only if the requesting user owns its domain.
Raises PermissionError on authorization failure.
"""
inc_job = IncJob.objects.get(id=job_id)
# Collect all domains the authenticated user is authorized to manage
admin = Administrator.objects.get(userName=request_user.username)
authorized_domains = set(
admin.domains.values_list('domainName', flat=True)
)
if inc_job.domain not in authorized_domains:
raise PermissionError(
f"User '{request_user.username}' is not authorized "
f"to access backup job {job_id} (domain: {inc_job.domain})"
)
return inc_job
def deleteBackup(request):
data = json.loads(request.body)
job_id = data.get('jobID')
try:
inc_job = _get_authorized_job(job_id, request.user)
except PermissionError as exc:
return JsonResponse({'error': str(exc)}, status=403)
except IncJob.DoesNotExist:
return JsonResponse({'error': 'Job not found'}, status=404)
result = remove_backup_snapshot(inc_job)
return JsonResponse({'status': 'deleted', 'job': job_id})
The same _get_authorized_job guard must be applied consistently to fetchRestorePoints and restorePoint. The critical principle is that the authorization check must be co-located with the resource retrieval — not applied earlier as a URL-level gate that can be bypassed by parameter manipulation.
For operators, the recommended remediation steps are:
- Update immediately by pulling the latest commit from the
masterbranch or applying commitb198460as a patch. - Audit access logs for unusual patterns in requests to the
/IncBackups/endpoint, particularly sequences of incrementingjobIDvalues originating from a single authenticated session. - Review all similar object-reference patterns across the CyberPanel codebase — any endpoint that retrieves a database record by a caller-supplied integer ID deserves the same scrutiny.
Our Take
IDOR vulnerabilities in multi-tenant control panels are not a novelty, but they remain stubbornly common because the failure mode is architectural rather than syntactic. A linter will not catch the absence of an authorization check; a developer focused on functional correctness will write code that does exactly what it is supposed to do — fetch the record and perform the operation — without pausing to ask whether the caller has the right to operate on that particular record.
The CyberPanel case is a textbook example of a missing object-level authorization check (OWASP API Security Top 10: API1, CWE-639). The sequential integer ID compounds the risk dramatically compared to an opaque UUID or a cryptographic token, but the authorization failure would be equally severe regardless of ID format — obscurity is not a substitute for access control enforcement.
For enterprises running control panels or any SaaS product with per-tenant resource partitioning, this class of vulnerability should be a first-class concern in both design reviews and automated testing pipelines. Every handler that retrieves a resource by ID must answer the question: is the currently authenticated principal allowed to touch this specific record?
Detection with SAST
SAST tooling detects IDOR vulnerabilities by tracing the flow of user-controlled data from HTTP request parameters through ORM query calls to sensitive operations, and checking whether an authorization predicate is applied before the operation executes.
For this specific pattern, Offensive360’s SAST engine flags the following:
- Taint source:
request.body,request.GET,request.POST— any Django HTTP input surface. - Dangerous sink:
Model.objects.get(id=<tainted>)orModel.objects.filter(pk=<tainted>).first()when the result is passed to a mutating or disclosure-sensitive function without an intervening ownership assertion. - Missing sanitizer: absence of a queryset filter that scopes the lookup to the authenticated user’s owned objects, e.g.,
Model.objects.get(id=<tainted>, owner=request.user)or an equivalent post-retrieval domain membership check.
This maps to CWE-639 (Authorization Bypass Through User-Controlled Key) and CWE-284 (Improper Access Control). In a DAST context, the vulnerability is detectable by replaying authenticated requests from one session using the jobID values observed in a second session and asserting that cross-tenant responses should return HTTP 403 rather than 200.
References
Detect this vulnerability class in your codebase
Offensive360 SAST scans your source code for CVE-2026-65917-class vulnerabilities and thousands of other patterns — across 60+ languages.