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.
Prevention & Best Practices
To avoid URL injection vulnerabilities in your code:
1. Always Validate Input Format
Use allowlists (like regex patterns) to validate that input matches expected formats before using it in URLs:
// Good: Validate before use
if (!/^[a-zA-Z0-9_-]+$/.test(userInput)) {
throw new Error("Invalid input format");
}
2. Use URL Construction Libraries
Instead of string concatenation, use URL construction methods that handle encoding:
// Better approach
const url = new URL('http://api.example.com/endpoint');
url.searchParams.append('qq', mid);
url.searchParams.append('skey', skey);
This automatically encodes special characters and prevents parameter injection.
3. Never Log Sensitive Data
Avoid logging complete URLs that contain authentication tokens, API keys, or other credentials. If logging is necessary for debugging:
// Redact sensitive parameters
const safeUrl = url.replace(/skey=[^&]+/, 'skey=REDACTED')
.replace(/pskey=[^&]+/, 'pskey=REDACTED');
logger.info(safeUrl);
4. Implement Input Sanitization Layers
Apply multiple layers of validation:
- Syntax validation: Check format (regex, type checking)
- Semantic validation: Verify the input makes sense for your use case
- Encoding: Properly encode before embedding in URLs
5. Use Security Linters
Tools like ESLint with security plugins can detect dangerous patterns:
npm install --save-dev eslint-plugin-security
Configure rules to flag unvalidated user input in URL construction.
6. Follow OWASP Guidelines
Refer to the OWASP Input Validation Cheat Sheet for comprehensive guidance on input validation strategies.
7. Consider Using Allowlist-Based Routing
For API integrations, maintain an allowlist of valid endpoints and parameters rather than constructing URLs dynamically from user 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.
References
- CWE-20: Improper Input Validation
- CWE-601: URL Redirection to Untrusted Site ('Open Redirect')
- OWASP Input Validation Cheat Sheet
- OWASP Server-Side Request Forgery Prevention Cheat Sheet
- Semgrep Rules for URL Injection Detection
- MDN Web Docs: URL API
- fix: the qq info lookup function constructs urls usi... in qqinfo.js