Back to Blog
critical SEVERITY8 min read

How File Type Validation Bypass Happens in Node.js Image Processing and How to Fix It

A critical file type validation vulnerability in `src/js/insert.js` allowed attackers to rename malicious executables with image extensions and bypass security checks. The fix implements magic byte verification to confirm actual file content matches the declared file type, preventing attackers from disguising dangerous files as harmless images.

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published August 30, 2026•Reviewed August 30, 2026

Answer Summary

The insertImagesFromPaths function in Node.js image processing code validated files based only on extension, allowing attackers to rename malware as `.png` or `.jpg` and have it processed as a valid image. The fix adds a `looksLikeImage()` function that verifies file magic bytes (the binary signature at the start of every file) against the extension, ensuring only genuine image files are processed regardless of their filename.

Vulnerability at a Glance

cweCWE-434 (Unrestricted Upload of File with Dangerous Type)
fixImplement magic byte verification to confirm file type matches content
riskRemote code execution, malware injection, arbitrary file processing
languageJavaScript (Node.js)
root causeValidation based solely on file extension without verifying actual file content
vulnerabilityFile Type Validation Bypass via Extension Spoofing

How File Type Validation Bypass Happens in Node.js Image Processing and How to Fix It

Introduction

In the image processing library, we discovered a critical file type validation bypass in src/js/insert.js at line 50. The insertImagesFromPaths function accepted user-supplied files for image processing, but it validated them based solely on file extension—a trivially spoofable property. An attacker could rename a malicious executable as malware.exe.png, and the application would process it as a valid image file without ever verifying the actual content. This vulnerability affects any downstream consumer of this Node.js library who processes user-uploaded or user-provided files.

The vulnerability was particularly dangerous because:
- File extensions are user-controlled: A simple rename bypasses the check
- No content verification: The code never examined the actual bytes of the file
- Production code path: This wasn't isolated to tests—it was in the active codebase
- Drag-and-drop vulnerability: The exploitation vector was trivial (user just drags a renamed file)

The Vulnerability Explained

The Problematic Code

The original insertImagesFromPaths function relied on a simple extension check:

const mimeFor = (ext) => ({ 
  png: 'image/png', 
  jpg: 'image/jpeg', 
  jpeg: 'image/jpeg', 
  gif: 'image/gif', 
  webp: 'image/webp', 
  bmp: 'image/bmp', 
  svg: 'image/svg+xml' 
}[ext] || 'image/png');

This function extracted the file extension and mapped it to a MIME type. If the extension was png, it assumed the file was a PNG image. There was no verification that the file's actual content matched this assumption.

Why This Is Dangerous

File extensions are metadata stored in the filesystem—they're not part of the file's actual content. An attacker can rename any file with any extension they want:

# Attacker renames a Windows executable to look like a PNG
mv malware.exe malware.png

# Or more cleverly:
mv backdoor.exe innocent-image.png.exe  # Double extension

When the application processes this file, it checks the extension, sees .png, and treats it as an image. The actual binary content—which contains executable code—is never examined.

Attack Scenario

  1. Attacker creates a malicious payload: Perhaps a Node.js script, a binary executable, or a crafted payload designed to exploit the image processing pipeline
  2. Attacker renames the file: Changes payload.exe to vacation-photo.png
  3. User is tricked into importing: Via drag-and-drop or file upload dialog, the user imports the file thinking it's an innocent image
  4. Application processes the malicious file: Because the extension is .png, the insertImagesFromPaths function accepts it
  5. Exploitation occurs: Depending on how the file is processed downstream, the malicious content could be executed, parsed, or used for further attacks

The vulnerability is likely exploitable because the file buffer is passed directly to image processing logic without any verification of actual file type.

Real-World Impact

For a library like this, the impact cascades to all downstream consumers:
- Any application using this library that accepts user-provided image files
- Any Electron/Tauri desktop application with drag-and-drop image import
- Any web application that processes user uploads through this library

An attacker could potentially achieve arbitrary code execution or inject malicious content into the application's processing pipeline.

The Fix

The fix implements magic byte verification—a technique that examines the binary signature at the start of every file to determine its true type, independent of the filename.

What Changed

A new function looksLikeImage() was added to verify file content:

/** Sniff the real file type from its magic bytes so a renamed executable can't pass as an image. */
function looksLikeImage(buf, ext) {
  const b = new Uint8Array(buf);
  if (ext === 'png') return b[0] === 0x89 && b[1] === 0x50 && b[2] === 0x4E && b[3] === 0x47;
  if (ext === 'jpg' || ext === 'jpeg') return b[0] === 0xFF && b[1] === 0xD8 && b[2] === 0xFF;
  if (ext === 'gif') return b[0] === 0x47 && b[1] === 0x49 && b[2] === 0x46;
  if (ext === 'bmp') return b[0] === 0x42 && b[1] === 0x4D;
  if (ext === 'webp') return b[0] === 0x52 && b[1] === 0x49 && b[2] === 0x46 && b[3] === 0x46;
  if (ext === 'svg') return /^\s*(<\?xml|<svg)/i.test(new TextDecoder().decode(b.slice(0, 256)));
  return false;
}

How Magic Bytes Work

Every file format has a unique binary signature—the "magic bytes" at the beginning of the file:

Format Magic Bytes (Hex) Hex Representation
PNG 89 50 4E 47 \x89PNG
JPEG FF D8 FF ÿØÿ
GIF 47 49 46 GIF
BMP 42 4D BM
WebP 52 49 46 46 RIFF (followed by WEBP)
SVG <?xml or <svg Text-based XML

These signatures are part of the file format specification and cannot be faked without creating a valid file of that type.

The Security Improvement

Now when a file is processed:

  1. Extension is checked: Is it .png?
  2. Magic bytes are verified: Do the first 4 bytes equal 0x89 0x50 0x4E 0x47 (PNG signature)?
  3. Both must match: Only if both the extension AND the magic bytes confirm it's a PNG will the file be processed

This means:
- ✅ A genuine PNG file renamed to .jpg will be rejected (extension doesn't match magic bytes)
- ✅ A malicious executable renamed to .png will be rejected (magic bytes don't match PNG signature)
- ✅ A genuine PNG file named photo.png will be accepted (both extension and magic bytes match)

Before and After

Before (Vulnerable):

// Only checks extension
const ext = filename.split('.').pop().toLowerCase();
const mime = mimeFor(ext);  // Trust the extension blindly
processImage(buffer, mime);  // Process whatever file this is

After (Secure):

// Checks both extension AND content
const ext = filename.split('.').pop().toLowerCase();
if (!looksLikeImage(buffer, ext)) {
  throw new Error(`File extension .${ext} does not match actual file type`);
}
const mime = mimeFor(ext);
processImage(buffer, mime);  // Now we know this is really an image

Prevention & Best Practices

1. Always Verify File Content, Never Trust Extensions

Extensions are user-controlled metadata. Always verify the actual file content:

// ❌ BAD: Only checks extension
const isImage = filename.endsWith('.png');

// ✅ GOOD: Checks magic bytes
const isImage = looksLikeImage(fileBuffer, extension);

2. Use Dedicated File Type Libraries

For production code, consider using battle-tested libraries:

// Using file-type library
import { fileTypeFromBuffer } from 'file-type';

const type = await fileTypeFromBuffer(buffer);
if (type?.mime !== 'image/png') {
  throw new Error('File is not a valid PNG');
}

3. Validate at Multiple Layers

  • Client-side: Quick UX feedback (but not security)
  • Server-side: Definitive validation using magic bytes
  • Content verification: Ensure the file can actually be processed as claimed
// Multi-layer validation
if (!filename.endsWith('.png')) return false;           // Quick check
if (!looksLikeImage(buffer, 'png')) return false;       // Magic bytes
try { 
  const image = await sharp(buffer).metadata();         // Can we actually process it?
  return image.format === 'png';
} catch (e) { 
  return false; 
}

4. Use Static Analysis to Catch This Pattern

Configure your security tools to flag:
- File operations that check only extension
- Buffer processing without type validation
- User-controlled filenames used directly for type determination

Semgrep rule pattern:

- id: nodejs-extension-only-validation
  pattern: |
    filename.split('.').pop()
    ...
    process$FUNC(buffer, ...)
  message: File type validated by extension only; use magic bytes instead

5. Reference Security Standards

  • CWE-434: Unrestricted Upload of File with Dangerous Type
  • CWE-345: Insufficient Verification of Data Authenticity
  • OWASP A4:2021: Insecure Deserialization (related: trusting untrusted data formats)

Key Takeaways

  • Extension-only validation is security theater: File extensions are trivially spoofed by renaming. Always verify actual file content using magic bytes.

  • The looksLikeImage() function implements a defense-in-depth check: It verifies that both the file extension AND the binary signature match known image formats, preventing renamed executables from passing validation.

  • Magic bytes are format-specific and cannot be faked: PNG files must start with 0x89 0x50 0x4E 0x47; JPEG files must start with 0xFF 0xD8 0xFF. These are part of the file format spec, not user-controlled.

  • This vulnerability affects downstream library consumers: Any application using this library for image processing inherited this risk until the fix was applied.

  • Drag-and-drop interfaces are attack vectors: User-friendly file import features are prime targets for this type of attack, making robust validation essential.

How Orbis AppSec Detected This

  • Source: User-controlled filename and file buffer from drag-and-drop or file upload in insertImagesFromPaths()
  • Sink: The mimeFor() function call at line 50 in src/js/insert.js that maps extension to MIME type without verifying file content
  • Missing control: No validation that the actual file bytes match the declared image format
  • CWE: CWE-434 (Unrestricted Upload of File with Dangerous Type)
  • Fix: Added looksLikeImage() function that verifies magic bytes before processing, ensuring the file's actual binary content matches the claimed image type

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

File type validation is a critical security control for any application that processes user-provided files. Relying solely on file extensions creates a false sense of security and opens the door to attacks where malicious files are disguised as harmless ones.

The fix in src/js/insert.js demonstrates the correct approach: verify file content using magic bytes, not just filename extensions. This simple but powerful technique ensures that only genuine image files are processed, regardless of what an attacker names the file.

As developers, we must remember that user input is untrusted—including filenames. By implementing magic byte verification and multi-layer validation, we can prevent attackers from bypassing our security controls through simple file renaming tricks.

For any library or application that processes user-supplied files, implement this fix today and audit your codebase for similar extension-only validation patterns.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1

Related Articles

high

CVE-2026-4800: lodash Template Imports Allow Code Execution

CVE-2026-4800 affects lodash's `_.template()` templating API, where untrusted input reaching the `imports` option can lead to arbitrary code execution. The fix was shipped as a dependency upgrade from lodash 4.17.21 to 4.18.1 in the project's lockfile, though the PR itself notes it was never verified against the actual code paths in this repository.

critical

proxy-addr 2.0.7 IP Spoofing: CVE-2026-90711 Trust Bypass

A critical vulnerability in proxy-addr 2.0.7 allowed attackers to spoof client IP addresses by manipulating X-Forwarded-For headers when the trust chain evaluation contained specific misconfigurations. The fix in version 2.0.8 hardens the trust evaluation logic to prevent IP address falsification in Express.js applications relying on this common middleware dependency.

high

TweenMax `_applyCycle` Prototype Pollution via vars.cycle Keys

A bundled copy of the TweenMax animation library copied attacker-influenceable `vars.cycle` property names straight onto a tween configuration object using an unguarded `for...in` loop, so a key named `__proto__`, `constructor`, or `prototype` was written through to the object's prototype chain. The fix adds an explicit key denylist to both copies of the `_applyCycle` helper so those three names are skipped during the merge. No CVE or GHSA is assigned; the issue is tracked as CWE-1321 (Improperl

high

picomatch 2.3.1 ReDoS: Extglob Pattern Catastrophic Backtracking

picomatch versions below 2.3.2, 3.0.2, or 4.0.4 contain a Regular Expression Denial of Service vulnerability in extglob pattern parsing. An attacker can cause catastrophic backtracking with patterns containing nested alternations and quantifiers, freezing any Node.js process that evaluates untrusted glob expressions.

critical

pet-window.js Dynamic Code Evaluation: CWE-94 Hardening via Number

The pet-window module constructed dynamic JavaScript by embedding raw configuration values into code strings. An attacker with local access could inject arbitrary JavaScript by modifying stored configuration. The fix replaces string interpolation with explicit Number() coercion and NaN validation for all numeric configuration parameters.

high

sanitizeUnicodeInput(): Fullwidth U+ Bypasses Codepoint Validation

The `sanitizeUnicodeInput()` helper used by the project character-range settings screen rewrote `U+` prefixes to `0x` and called `parseInt()`, but never normalized its argument first. Compatibility-equivalent forms such as fullwidth `U+`, superscript digits, or mathematical alphanumerics never matched the `/U\+/gi` regex, fell through to the `else return inputString` branch, and were handed back to callers verbatim as "sanitized" values. The fix inserts a `String.prototype.normalize('NFKC')` pas