Introduction
In apps/panel/panel.js, a high-severity signature verification bypass was discovered at line 65. The code handles share code verification using RSA-SHA256 signatures, but the JMkey variable—containing the signature data—was passed directly to Buffer.from(JMkey, 'hex') without any format or content validation.
This matters because signature verification is a critical security boundary. When the verification code accepts malformed input, it can fail silently or produce unexpected results, potentially allowing attackers to bypass the entire signature check and inject malicious share codes into the system.
The vulnerable pattern looked like this:
let Tex = crypto.createVerify('RSA-SHA256')
Tex.update(ccb, 'hex')
let acc = Buffer.from(JMkey, 'hex') // JMkey used without validation!
The Vulnerability Explained
What's Actually Happening
The panel.js file implements a share code verification system that uses RSA-SHA256 digital signatures. When a user submits a share code, the system extracts a signature value (JMkey) and attempts to verify it against a public key.
The core issue is that Buffer.from(JMkey, 'hex') has permissive behavior with malformed input:
- Empty strings:
Buffer.from('', 'hex')returns an empty buffer without error - Odd-length strings:
Buffer.from('abc', 'hex')silently drops the last character - Invalid characters: Non-hex characters may be silently ignored or produce unexpected bytes
- Null/undefined: These can cause the verification to behave unpredictably
The Attack Scenario
An attacker crafts a malicious share code with a specially formatted JMkey value. Consider this exploitation path:
- Attacker creates a share code with
JMkeyset to an odd-length hex string like"abc" Buffer.from("abc", 'hex')silently produces a buffer from just"ab"(dropping thec)- The truncated signature data may cause the verification to fail in unexpected ways—or worse, if the attacker can control other parameters, they might craft a collision
- If the error handling doesn't properly reject the request, the malicious data could be accepted
The real-world impact is severe: this is a Node.js library where vulnerabilities affect all downstream consumers. Any application using this package's share code feature could be tricked into accepting forged or tampered data.
Why Silent Failures Are Dangerous
Unlike many functions that throw errors on bad input, Buffer.from() with hex encoding is designed to be lenient. This is useful for data processing but dangerous for security-critical code:
// These all "succeed" without throwing errors:
Buffer.from('', 'hex') // Empty buffer
Buffer.from('zz', 'hex') // Empty buffer (invalid hex)
Buffer.from('abc', 'hex') // Buffer with 1 byte (odd length truncated)
When signature verification code doesn't validate input, these silent behaviors can cascade into security bypasses.
The Fix
The fix adds explicit validation of JMkey before any cryptographic operations occur. Here's the before and after comparison:
Before (Vulnerable)
try {
let Tex = crypto.createVerify('RSA-SHA256')
Tex.update(ccb, 'hex')
let acc = Buffer.from(JMkey, 'hex')
// ... verification continues
}
After (Secure)
if (!JMkey || !/^[0-9a-fA-F]+$/.test(JMkey) || JMkey.length % 2 !== 0) {
e.reply(`[liangshi-calc] 不正确的签名,可能是数据缺失或被篡改`);
return false
}
try {
let Tex = crypto.createVerify('RSA-SHA256')
Tex.update(ccb, 'hex')
let acc = Buffer.from(JMkey, 'hex')
// ... verification continues
}
What the Validation Does
The new validation line performs three critical checks:
!JMkey: Rejects null, undefined, or empty strings!/^[0-9a-fA-F]+$/.test(JMkey): Ensures only valid hexadecimal characters (0-9, a-f, A-F) are presentJMkey.length % 2 !== 0: Ensures even length (each byte requires exactly 2 hex characters)
This fix was applied to both verification code paths in the file (lines 68 and 132), ensuring consistent protection throughout the module.
Why This Works
By validating before processing:
- Malformed input is rejected immediately with a clear error message
- The cryptographic code only receives properly formatted data
- Silent failures become impossible because invalid input never reaches Buffer.from()
Prevention & Best Practices
1. Validate Before Cryptographic Operations
Always validate input parameters before passing them to cryptographic functions:
function isValidHexString(str) {
return str &&
typeof str === 'string' &&
/^[0-9a-fA-F]+$/.test(str) &&
str.length % 2 === 0;
}
if (!isValidHexString(signature)) {
throw new Error('Invalid signature format');
}
2. Fail Closed, Not Open
When signature verification encounters any error or unexpected condition, the default behavior should be to reject the data:
// Good: Explicit rejection on any failure
if (!isValidSignature(data, signature)) {
return { valid: false, error: 'Verification failed' };
}
// Bad: Implicit acceptance if verification "doesn't fail"
try {
verify(data, signature);
return { valid: true };
} catch (e) {
// What if verification silently produced wrong results?
}
3. Use Type-Safe Wrappers
Create wrapper functions that enforce validation:
function safeHexToBuffer(hexString) {
if (!hexString || !/^[0-9a-fA-F]+$/.test(hexString) || hexString.length % 2 !== 0) {
throw new TypeError('Invalid hex string');
}
return Buffer.from(hexString, 'hex');
}
4. Security Testing for Cryptographic Code
Test signature verification with edge cases:
- Empty strings
- Odd-length hex strings
- Strings with invalid characters
- Very long strings
- Unicode characters that look like hex digits
Key Takeaways
- Never trust
Buffer.from()to validate hex input—it silently handles malformed data in ways that can break security assumptions - The
JMkeysignature variable required explicit regex validation (/^[0-9a-fA-F]+$/) before being used in RSA-SHA256 verification - Both verification code paths in
panel.js(lines 68 and 132) needed the same fix—inconsistent validation creates exploitable gaps - Silent failures in cryptographic code are security vulnerabilities—always validate input so errors are explicit and handled properly
- Library vulnerabilities cascade to all consumers—this Node.js package fix protects every downstream application
How Orbis AppSec Detected This
- Source: The
JMkeyvariable extracted from user-supplied share code data - Sink:
Buffer.from(JMkey, 'hex')atapps/panel/panel.js:65and line 132 - Missing control: No validation of hex string format, length, or content before cryptographic processing
- CWE: CWE-347 (Improper Verification of Cryptographic Signature)
- Fix: Added regex validation (
/^[0-9a-fA-F]+$/) and length check (JMkey.length % 2 !== 0) before Buffer conversion
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 signature verification bypass demonstrates a subtle but critical security issue: cryptographic code that doesn't validate its inputs can fail in ways that benefit attackers. The Buffer.from() function's lenient handling of malformed hex strings, combined with the lack of input validation for JMkey, created a path for attackers to potentially bypass signature verification entirely.
The fix is straightforward—a single line of regex validation before processing—but its impact is significant. By ensuring JMkey contains only valid hexadecimal characters and has an even length, the code now fails explicitly on bad input rather than proceeding with corrupted data.
When writing cryptographic verification code, remember: validate everything, trust nothing, and make failures loud and explicit.