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()

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.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #27

Related Articles

critical

deleteNestedProperty Prototype Pollution via Dot-Notation Path

The `deleteNestedProperty` function in propertyUtils.ts allowed attackers to manipulate JavaScript object prototypes by passing specially crafted dot-notation paths like `__proto__.polluted`. A fix now blocks dangerous keys before processing, preventing prototype pollution attacks that could affect all objects in the application.

high

How Denial of Service via Infinite Loop Happens in JavaScript Dependencies and How to Fix It

CVE-2026-67213 is a high-severity denial of service vulnerability in nanoid before version 5.1.6 that triggers an infinite loop during random ID generation when processing specially crafted input. We upgraded nanoid across the entire dependency tree to patch this flaw and prevent attackers from freezing application threads. This fix ensures that ID generation remains resilient even when handling adversarial input patterns.

high

How Sensitive Data Exposure happens in Zotero plugins and how to fix it

A high-severity data exposure vulnerability in `Zotero.ts` automatically transmitted complete document metadata—including private notes, attachment paths, and tags—to external LLM services without user consent. The fix replaces broad `item.toJSON()` serialization with explicit field selection, sending only essential bibliographic data.

high

How missing dependency update cooldowns happen in GitHub Dependabot configurations and how to fix it

A semgrep scan flagged `.github/dependabot.yml` for lacking a cooldown period, meaning Dependabot would immediately propose updates to brand-new package versions across npm, Bundler, and Docker ecosystems. The fix adds a `cooldown: default-days: 7` block to every `package-ecosystem` entry, forcing a one-week waiting period before newly published releases are considered — reducing exposure to malicious or unstable package drops.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

critical

How Unbounded WebSocket Message Handling Causes Resource Exhaustion in Node.js and How to Fix It

The WebSocketCrossServerAdapter class in a popular Node.js WebSocket library lacked any rate limiting on inbound messages, allowing attackers to flood Redis nodes and WebSocket servers with high-volume traffic. The fix introduces a configurable `rateLimit` option that caps messages per connection per second, preventing resource exhaustion while preserving legitimate functionality.