+ "details": "## Summary\n\nThe `uploadVideoToLinkedIn()` method in the SocialMediaPublisher plugin constructs a shell command by directly interpolating an upload URL received from LinkedIn's API response, without sanitization via `escapeshellarg()`. If an attacker can influence the LinkedIn API response (via MITM, compromised OAuth token, or API compromise), they can inject arbitrary OS commands that execute as the web server user.\n\n## Details\n\nThe vulnerability exists in `plugin/SocialMediaPublisher/Objects/SocialUploader.php`.\n\nThe `initializeLinkedInUploadSession()` method (line 649) sends a POST request to `https://api.linkedin.com/rest/videos?action=initializeUpload` and parses the JSON response at line 693:\n\n```php\n// SocialUploader.php:693\n$responseArray = json_decode($response, true);\n```\n\nThe parsed `uploadInstructions` array is iterated at line 532, and each `uploadUrl` is passed to `uploadVideoToLinkedIn()` at line 542:\n\n```php\n// SocialUploader.php:542\n$uploadResponse = self::uploadVideoToLinkedIn($instruction['uploadUrl'], $tmpFile);\n```\n\nThe `uploadVideoToLinkedIn()` method (line 711) constructs a shell command by directly concatenating both `$uploadUrl` and `$filePath` into a string passed to `exec()`:\n\n```php\n// SocialUploader.php:713-720\n$shellCmd = 'curl -v -H \"Content-Type:application/octet-stream\" --upload-file \"' .\n $filePath . '\" \"' .\n $uploadUrl . '\" 2>&1';\n\n_error_log(\"Upload Video Shell Command:\\n\" . $shellCmd);\n\nexec($shellCmd, $o);\n```\n\nNeither `$uploadUrl` nor `$filePath` is sanitized with `escapeshellarg()`. A malicious URL such as `https://uploads.linkedin.local\" ; id ; echo \"` would break out of the quoted string and execute arbitrary commands.\n\nThe `$uploadUrl` originates from LinkedIn's API response — a trusted third-party source over HTTPS — so exploitation requires compromising that response (MITM at CA level, compromised OAuth token leading to attacker-controlled API responses, or LinkedIn API compromise). This makes the attack complexity high, but the missing sanitization is a defense-in-depth failure that could become critical if the trust boundary is ever violated.\n\n## PoC\n\nThis vulnerability requires manipulating the LinkedIn API response. A simulated proof-of-concept using a local proxy:\n\n**Step 1:** Set up a proxy that intercepts the LinkedIn API response and replaces the `uploadUrl` field:\n\n```json\n{\n \"value\": {\n \"uploadInstructions\": [\n {\n \"uploadUrl\": \"https://example.com\\\" ; id > /tmp/pwned ; echo \\\"\",\n \"firstByte\": 0,\n \"lastByte\": 1024\n }\n ],\n \"uploadToken\": \"token123\",\n \"video\": \"urn:li:video:123\"\n }\n}\n```\n\n**Step 2:** The resulting shell command becomes:\n\n```bash\ncurl -v -H \"Content-Type:application/octet-stream\" --upload-file \"/tmp/tmpfile\" \"https://uploads.linkedin.local\" ; id > /tmp/pwned ; echo \"\" 2>&1\n```\n\n**Step 3:** The `id` command executes as the web server user, writing output to `/tmp/pwned`.\n\n**Step 4:** Verify:\n\n```bash\ncat /tmp/pwned\n# uid=33(www-data) gid=33(www-data) groups=33(www-data)\n```\n\n## Impact\n\n- **Remote Code Execution:** If the LinkedIn API response is compromised, an attacker gains arbitrary command execution as the web server user (`www-data`).\n- **Confidentiality:** Full read access to application source code, configuration files (including database credentials), and any data accessible to the web server process.\n- **Integrity:** Ability to modify application files, inject backdoors, or alter database records.\n- **Practical risk is low** due to the high attack complexity — exploitation requires compromising a trusted HTTPS API response from LinkedIn. This is primarily a defense-in-depth issue.\n\n## Recommended Fix\n\nSanitize both `$uploadUrl` and `$filePath` with `escapeshellarg()` before interpolation into the shell command. Alternatively, replace the `exec()` call with PHP's native cURL functions (which are already used elsewhere in the same class):\n\n**Option 1 — Minimal fix with `escapeshellarg()`:**\n\n```php\n// plugin/SocialMediaPublisher/Objects/SocialUploader.php:711-715\nstatic function uploadVideoToLinkedIn($uploadUrl, $filePath)\n{\n $shellCmd = 'curl -v -H \"Content-Type:application/octet-stream\" --upload-file ' .\n escapeshellarg($filePath) . ' ' .\n escapeshellarg($uploadUrl) . ' 2>&1';\n```\n\n**Option 2 — Replace shell exec with native PHP cURL (preferred):**\n\n```php\nstatic function uploadVideoToLinkedIn($uploadUrl, $filePath)\n{\n $ch = curl_init();\n curl_setopt($ch, CURLOPT_URL, $uploadUrl);\n curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/octet-stream']);\n curl_setopt($ch, CURLOPT_PUT, true);\n curl_setopt($ch, CURLOPT_INFILE, fopen($filePath, 'r'));\n curl_setopt($ch, CURLOPT_INFILESIZE, filesize($filePath));\n curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\n curl_setopt($ch, CURLOPT_HEADER, true);\n curl_setopt($ch, CURLOPT_VERBOSE, true);\n\n $response = curl_exec($ch);\n $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);\n $headers = substr($response, 0, $headerSize);\n curl_close($ch);\n\n // Extract ETag from response headers\n $matches = [];\n preg_match('/(etag:)(\\s?)(.*)(\\n)/i', $headers, $matches);\n $etag = isset($matches[3]) ? trim($matches[3]) : null;\n\n // ... rest of function\n}\n```\n\nOption 2 is strongly preferred as it eliminates the shell execution entirely, removing the injection surface and aligning with the PHP cURL usage already present in `initializeLinkedInUploadSession()` on line 664.",
0 commit comments