Back to Blog
high SEVERITY6 min read

How signature verification bypass happens in Node.js crypto and how to fix it

A high-severity signature verification bypass was discovered in `apps/panel/panel.js` where the `JMkey` variable was passed directly to `Buffer.from(JMkey, 'hex')` without validating its format. An attacker could supply a malformed hex string to cause silent failures or unexpected behavior in the RSA-SHA256 verification process, potentially bypassing signature checks entirely. The fix adds strict hex format validation before processing.

O
By Orbis AppSec
Published July 31, 2026Reviewed July 31, 2026

Answer Summary

This is a signature verification bypass vulnerability (CWE-347) in Node.js where `Buffer.from(JMkey, 'hex')` processes user-controlled input without format validation. Malformed hex strings can cause silent failures in the RSA-SHA256 verification flow, allowing attackers to bypass signature checks. The fix validates that `JMkey` exists, contains only valid hexadecimal characters (`/^[0-9a-fA-F]+$/`), and has an even length before processing.

Vulnerability at a Glance

cweCWE-347 (Improper Verification of Cryptographic Signature)
fixAdded regex validation to ensure JMkey is a valid, even-length hex string
riskAttackers can bypass signature verification to inject malicious data
languageNode.js (JavaScript)
root causeMissing validation of JMkey hex string before Buffer.from() conversion
vulnerabilitySignature Verification Bypass via Malformed Hex Input

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:

  1. Empty strings: Buffer.from('', 'hex') returns an empty buffer without error
  2. Odd-length strings: Buffer.from('abc', 'hex') silently drops the last character
  3. Invalid characters: Non-hex characters may be silently ignored or produce unexpected bytes
  4. 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:

  1. Attacker creates a share code with JMkey set to an odd-length hex string like "abc"
  2. Buffer.from("abc", 'hex') silently produces a buffer from just "ab" (dropping the c)
  3. 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
  4. 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:

  1. !JMkey: Rejects null, undefined, or empty strings
  2. !/^[0-9a-fA-F]+$/.test(JMkey): Ensures only valid hexadecimal characters (0-9, a-f, A-F) are present
  3. JMkey.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 JMkey signature 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 JMkey variable extracted from user-supplied share code data
  • Sink: Buffer.from(JMkey, 'hex') at apps/panel/panel.js:65 and 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.

References

Frequently Asked Questions

What is a signature verification bypass?

A signature verification bypass occurs when an application fails to properly validate cryptographic signatures, allowing attackers to submit unsigned or maliciously signed data that the system accepts as legitimate.

How do you prevent signature verification bypass in Node.js?

Always validate input parameters before cryptographic operations—check that hex strings contain only valid characters (0-9, a-f, A-F), have appropriate lengths, and are not null or undefined before passing them to Buffer.from() or crypto functions.

What CWE is signature verification bypass?

CWE-347 (Improper Verification of Cryptographic Signature) covers vulnerabilities where an application does not properly verify that data has been signed by a trusted entity.

Is try-catch enough to prevent signature verification bypass?

No, try-catch alone is insufficient because Buffer.from() with 'hex' encoding may silently produce unexpected results with malformed input rather than throwing an error, allowing the verification to proceed with corrupted data.

Can static analysis detect signature verification bypass?

Yes, static analysis tools can detect patterns where user-controlled input flows into cryptographic functions without validation, flagging potential signature verification bypass vulnerabilities.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #27

Related Articles

medium

How OAuth token audience bypass happens in Node.js serverless functions and how to fix it

A critical OAuth authentication vulnerability in a Netlify serverless function allowed any valid Google OAuth token—even those issued to completely different applications—to authenticate successfully. The fix adds proper audience (aud) claim verification using Google's tokeninfo endpoint to ensure only tokens issued specifically for this application are accepted.

critical

How insecure nonce generation with Math.random() happens in Node.js HTTP Digest authentication and how to fix it

A critical vulnerability was discovered in `lib/cam.js` where the HTTP Digest authentication client nonce (cnonce) was generated using `Math.random().toString(36)` — a cryptographically insecure source of randomness. An attacker observing authentication exchanges could predict future cnonce values and forge valid authentication responses. The fix replaces this with `crypto.randomBytes(4).toString('hex')`, providing cryptographically secure random values.

critical

How missing authorization enforcement happens in Node.js Express routers and how to fix it

A critical authorization bypass was discovered in lib/router.js where readOnly and noDelete configuration options were only enforced through UI controls, not server-side middleware. Any authenticated user could bypass these restrictions by sending direct HTTP requests to perform destructive operations like database deletion or document modification. The fix adds Express middleware that enforces these security modes at the server level, blocking POST, PUT, and DELETE requests when appropriate.

critical

How command injection happens in Java Runtime.exec() and how to fix it

A critical command injection vulnerability was discovered in `ProcessUtils.java` where process IDs were concatenated directly into shell command strings passed to `Runtime.getRuntime().exec(String)`. This allowed shell metacharacter injection that could execute arbitrary commands. The fix switches to the array-based `exec(String[])` overload, which bypasses shell interpretation entirely.

critical

How Algolia API key exposure happens in EJS templates and how to fix it

A critical vulnerability in `layout/_plugins/global/config.ejs` was exposing Algolia API credentials — including `appId` and `apiKey` — directly in rendered HTML, making them visible to any user who inspects the page source. The fix removes raw credential interpolation from the template and replaces unsafe string embedding with properly sanitized output using `JSON.stringify()`. This eliminates the risk of unauthorized Algolia API access by anyone who visits the page.

high

How unauthenticated endpoint exposure happens in Node.js Express and how to fix it

A high-severity vulnerability in the AgenticATODetectionService allowed unauthenticated users to access sensitive agent status data, trigger detection scans, and view security alerts. The fix adds authentication middleware to four critical API endpoints, ensuring only authorized users can access these sensitive operations.