Back to Blog
critical SEVERITY7 min read

How URL Injection via Unvalidated User Input happens in Node.js and how to fix it

A critical URL injection vulnerability in the QQ info lookup feature allowed attackers to manipulate API request parameters by sending specially crafted messages. Without proper input validation, user-controlled data was directly embedded into external API URLs, potentially exposing sensitive authentication credentials (skey and pskey) to attacker-controlled servers.

O
By Orbis AppSec
Published August 5, 2026Reviewed August 5, 2026

Answer Summary

This is a URL injection vulnerability (CWE-20: Improper Input Validation) in Node.js that occurs when the qqinfo.js file constructs external API URLs using unvalidated user input. The `mid` variable, derived from user messages, was embedded directly into HTTP requests without checking if it contains only valid QQ numbers. The fix adds a regex validation `/^\d+$/.test(mid)` to ensure only numeric input is accepted, preventing attackers from injecting malicious URL parameters that could leak skey and pskey authentication credentials to third-party servers.

Vulnerability at a Glance

cweCWE-20 (Improper Input Validation)
fixAdded regex validation `/^\d+$/.test(mid)` to ensure only numeric QQ IDs are accepted
riskAuthentication credentials (skey, pskey) could be leaked to attacker-controlled servers
languageJavaScript (Node.js)
root causeUser input embedded in external API URL without validation
vulnerabilityURL Injection via Unvalidated User Input

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:

  1. 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.

  2. Parameter Pollution: By injecting &pskey=attacker_token, the attacker could override the legitimate pskey parameter, potentially causing authentication failures or logging the real credentials alongside attacker-controlled values.

  3. 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 skey and pskey values.

  4. 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:

  1. Strict Format Enforcement: Only numeric QQ IDs are accepted. Any attempt to inject special characters like &, =, /, ?, or . will fail validation.

  2. 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.

  3. 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 mid variable 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

Frequently Asked Questions

What is URL injection via unvalidated user input?

URL injection occurs when user-controlled data is embedded into URLs without validation, allowing attackers to manipulate request parameters, redirect requests to malicious servers, or inject additional query parameters that leak sensitive data.

How do you prevent URL injection in Node.js?

Validate and sanitize all user input before embedding it in URLs. Use allowlists (like regex patterns) to ensure input matches expected formats, encode special characters, and avoid directly concatenating user input into URL strings. Consider using URL parsing libraries that handle encoding automatically.

What CWE is URL injection via unvalidated user input?

This falls under CWE-20 (Improper Input Validation) and can lead to CWE-601 (URL Redirection to Untrusted Site) or CWE-918 (Server-Side Request Forgery) depending on the exploitation scenario.

Is URL encoding enough to prevent URL injection?

No. URL encoding only prevents syntax errors and basic injection, but doesn't validate that the input is semantically correct or safe. Attackers can still inject valid but malicious parameters. You need input validation (allowlists, format checks) combined with encoding for comprehensive protection.

Can static analysis detect URL injection vulnerabilities?

Yes. Static analysis tools can trace data flow from user input sources to URL construction sinks, identifying cases where unvalidated user data is embedded in URLs. Tools like Semgrep, CodeQL, and specialized security scanners can detect these patterns automatically.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #48

Related Articles

critical

How Prototype Pollution Denial of Service Happens in Node.js HTTP Libraries and How to Fix It

A critical prototype pollution vulnerability in axios versions 1.12.0 and earlier could allow attackers to trigger denial of service attacks by poisoning the configuration object through the `__proto__` key. The vulnerability was fixed by upgrading axios to 1.13.5 and updating related dependencies like follow-redirects to 1.16.0, which implements stricter input validation in the mergeConfig function.

critical

How SQL Injection happens in PHP PDO queries and how to fix it

A critical SQL injection vulnerability was discovered in the `getOfficialContests()` method of ContestRepository.php, where the `$site_id` parameter was directly interpolated into a SQL query string instead of using prepared statements. This vulnerability allowed attackers to inject arbitrary SQL commands and potentially access or manipulate the entire contest database. The fix replaced `pdo->query()` with `pdo->prepare()` and proper parameter binding.

critical

How broken authentication happens in Node.js Express APIs and how to fix it

A critical authentication bypass in the `/api/posts` endpoint allowed any unauthenticated user to create, update, or delete posts without verification. The POST endpoint had zero authentication checks, while PUT and DELETE endpoints used a trivially bypassable username comparison that attackers could forge by simply including the target username in their request body. The fix validates user identity by looking up the userId in the database before any post operations.

critical

How Server-Side Request Forgery (SSRF) happens in JavaScript and how to fix it

A critical Server-Side Request Forgery (SSRF) vulnerability was discovered in `libraries/microworlds/video.js` where the `VideoCanvasWrapper.loadVideo()` function passed user-controlled URLs directly to `fetch()` without any validation. An attacker could exploit this by supplying URLs pointing to internal services, localhost endpoints, or malicious external servers. The fix introduces strict URL parsing and protocol validation before any network request is made.

critical

How Denial of Service via Crafted ZIP File Happens in Node.js and How to Fix It

A critical denial-of-service vulnerability (CVE-2026-39244) in adm-zip 0.5.10 allowed attackers to craft malicious ZIP files that triggered excessive memory allocation, potentially crashing the Node.js process. The fix upgrades adm-zip to version 0.6.0, which includes proper memory allocation limits when parsing ZIP entries. This vulnerability was discovered in the `solarIncidenceService.js` service, where uploaded ZIP files are processed without sandboxing.

critical

How Denial of Service Vulnerabilities Happen in QUIC Protocol Implementations and How to Fix Them

The quinn-proto library, a critical component for QUIC protocol implementations, contained a denial of service vulnerability (CVE-2026-31812) that could be triggered by specially crafted QUIC Initial packets. A security update from version 0.11.13 to 0.11.14 tightens the handling of untrusted network input, preventing attackers from exhausting server resources through malformed packets.