Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions src/PBXCoreREST/Lib/ModulesManagementProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,18 @@ public static function callBack(array $request): PBXApiResult
$res->processor = __METHOD__;
switch ($action) {
case 'startDownload':
$module = $request['data']['uniqid'];
$url = $request['data']['url'];
$md5 = $request['data']['md5'];
$res = StartDownloadAction::main($module, $url, $md5);
// REST v3 carries the module in the path as id; uniqid is the legacy field (#1250).
// Both actions reject a bad or empty id with 400 before touching the filesystem.
$res = StartDownloadAction::main(
self::stringParam($data, 'uniqid') ?: self::stringParam($data, 'id'),
self::stringParam($data, 'url'),
self::stringParam($data, 'md5')
);
break;
case 'getDownloadStatus':
$module = $request['data']['uniqid'];
$res = DownloadStatusAction::main($module);
$res = DownloadStatusAction::main(
self::stringParam($data, 'uniqid') ?: self::stringParam($data, 'id')
);
break;
case 'installFromPackage':
$filePath = $data['filePath'];
Expand Down Expand Up @@ -341,6 +345,15 @@ private static function logOperationInitiator(
SystemMessages::sysLogMsg(__CLASS__, 'Module operation started: ' . $encoded, LOG_WARNING);
}

/**
* A request field as a string; missing or non-string values (e.g. uniqid[]=x) become ''.
*/
private static function stringParam(array $data, string $key): string
{
$value = $data[$key] ?? '';
return is_string($value) ? $value : '';
}

/**
* Emergency switch: '1' routes install/update/uninstall back to the
* legacy mutex-driven pipeline for one release cycle.
Expand Down
24 changes: 21 additions & 3 deletions tests/api/test_39_modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,19 +73,37 @@ def test_04_get_download_status_rejects_unsafe_uniqid(self, api_client):
"""GET /modules/{id}:getDownloadStatus must reject a uniqid that is not a safe directory name.

WHY: the uniqid is used to build the download directory path under the upload dir.
The id goes in a JSON body: the nginx WAF answers 403 to ".." in the query string
before PHP runs, but it does not scan GET bodies, and the API reads them.
GET bodies are ignored since #1161 and the WAF answers 403 to ".." in the query
string, so the reachable vector is a query uniqid that is not a plain name.
"""
# Sent raw so that a non-2xx answer does not raise
raw = api_client.session.get(
f"{api_client.base_url}/modules/ModuleTemplate:getDownloadStatus",
json={'uniqid': '../../evil'},
params={'uniqid': 'evil/name'},
headers=api_client._get_headers(),
timeout=30
)
assert raw.status_code == 400, \
f"Expected HTTP 400 for unsafe uniqid, got {raw.status_code}: {raw.text}"
assert raw.json().get('result') is False, f"Expected result=false, got: {raw.text}"

def test_05_get_download_status_uses_path_id(self, api_client):
"""GET /modules/{id}:getDownloadStatus must read the module from the path (#1250).

Without a download in progress the action reports NOT_FOUND; before the fix the
processor ignored the path id and failed with 422 "Undefined array key", and a
non-string uniqid (uniqid[]=x) failed with a TypeError. Such a uniqid is ignored.
"""
for params in ({}, {'uniqid[]': 'ModuleTemplate'}):
raw = api_client.session.get(
f"{api_client.base_url}/modules/ModuleTemplate:getDownloadStatus",
params=params,
headers=api_client._get_headers(),
timeout=30
)
data = raw.json().get('data') or {}
assert data.get('d_status') == 'NOT_FOUND', \
f"Expected d_status=NOT_FOUND for {params or 'path id only'}, got {raw.status_code}: {raw.text}"

if __name__ == '__main__':
pytest.main([__file__, '-v', '-s'])
Loading