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
Published August 30, 2026Reviewed 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

How Regular Expression Denial of Service (ReDoS) Happens in Node.js trim-newlines and How to Fix It

CVE-2021-33623 exposed a Regular Expression Denial of Service (ReDoS) vulnerability in the npm package `trim-newlines` versions 1.0.0 and earlier. The vulnerable `.end()` method used an inefficient regex pattern that could cause severe performance degradation when processing malicious input. Upgrading to version 4.0.1 patches the regex implementation and eliminates the attack surface.

critical

How CSS Injection via Weak Pattern Validation happens in Vue.js and how to fix it

A critical CSS injection vulnerability in `testpage/App.vue` allowed attackers to bypass weak HTML5 pattern validation and load malicious stylesheets. The fix replaces direct variable assignment with a hardened `setCustomStylesheetHref()` method using strict regex validation.

critical

How Unvalidated Dynamic Component Loading happens in TypeScript/Viewi and how to fix it

A critical vulnerability in Viewi's component loader allowed attackers to inject malicious JavaScript through compromised or MITM-attacked external component servers. The fix adds proper HTTP response validation before parsing dynamically fetched JSON components.

high

How Denial of Service via Crafted ZIP File happens in Node.js and how to fix it

CVE-2026-39244 is a high-severity denial of service vulnerability in the adm-zip npm package that allows attackers to crash Node.js applications by uploading maliciously crafted ZIP files. The fix upgrades adm-zip from version 0.5.16 to 0.6.0, which adds proper memory bounds checking to prevent excessive allocation during archive extraction.

critical

How prototype pollution happens in JavaScript AST traversal and how to fix it

A critical prototype pollution primitive was fixed in `src/traverse/estraverse` where visitor-supplied child keys were merged with `Object.assign(Object.create(this.__keys), visitor.keys)`. Because `Object.assign` uses assignment semantics, a key literally named `__proto__` reached the `Object.prototype` setter and rewired the prototype chain of the traversal key map instead of being stored as data. The fix replaces the merge with an object spread (`{ ...VisitorKeys, ...visitor.keys }`), which *

critical

How SQL injection happens in Python DuckDB view creation and how to fix it

A critical SQL injection flaw in `python/src/idx/api.py:265` built five DuckDB `CREATE VIEW` statements with Python f-strings, interpolating a filesystem path directly into SQL text. The fix replaces the interpolated path with a bound parameter (`read_parquet(?)`) and moves the view names into a hardcoded, non-interpolated statement map — eliminating any path where filenames or directory values can alter SQL structure.