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.


References

Frequently Asked Questions

What is file type validation bypass?

A security flaw where an application trusts only the file extension to determine file type, allowing attackers to rename a malicious executable (e.g., `malware.exe`) as `malware.png` and have it processed as an image.

How do you prevent file type validation bypass in Node.js?

Always verify file content using magic bytes (file signatures) rather than relying on extensions. Libraries like `file-type` or manual magic byte checking ensure the actual file content matches the declared type.

What CWE is file type validation bypass?

CWE-434 (Unrestricted Upload of File with Dangerous Type) and CWE-434 often overlaps with CWE-345 (Insufficient Verification of Data Authenticity).

Is checking the file extension enough to prevent file type bypass?

No. Extensions are trivially spoofed by renaming files. You must verify the actual binary content using magic bytes, file signatures, or dedicated file type detection libraries.

Can static analysis detect file type validation bypass?

Yes. Tools can flag functions that only check file extensions without verifying content, or that process file buffers without type validation. Semgrep rules can identify this pattern.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1

Related Articles

critical

How URL Injection happens in Node.js template literals and how to fix it

A URL injection vulnerability in `lib/client.js` allowed user-controlled `repo`, `branch`, and `file` parameters to be interpolated directly into fetch URLs without encoding, enabling potential URL manipulation and request hijacking. The fix introduces per-segment percent-encoding via a new `encodePathSegments` helper, neutralizing special characters before they reach the URL construction layer. This closes an exploit primitive that automated attack tooling could chain with other weaknesses.

medium

How XML External Entity (XXE) Injection happens in Python and how to fix it

A high-severity XML External Entity (XXE) vulnerability was discovered in `utils/commands_extractors/find_java_repo_commands.py` where Python's native `xml.etree.ElementTree` library was used to parse potentially untrusted XML input. The fix replaces it with `defusedxml.ElementTree`, which disables external entity processing by default, preventing attackers from reading sensitive files or making unauthorized network requests.

high

How Cache-Control Header Injection Happens in Node.js HTTP Libraries and How to Fix It

CVE-2026-13697 is a high-severity vulnerability in the undici HTTP client library where the cache interceptor mishandles malformed Cache-Control directives, potentially leading to information disclosure and denial of service attacks. Upgrading from undici 7.28.0 to 7.29.0 (or 8.9.0 for v8 users) patches this vulnerability by implementing stricter validation of Cache-Control headers. This fix is critical for any Node.js application that relies on undici for HTTP requests, especially those handlin

critical

How XML Multiple Root Element Injection happens in Node.js and how to fix it

The foam3 project contained a critical vulnerability in xmldom version 0.6.0 that allowed attackers to create malformed XML documents with multiple root elements, violating the XML specification and potentially bypassing security validations. The fix removed the vulnerable xmldom dependency entirely from package.json and package-lock.json, eliminating the attack surface.

critical

How Prototype Pollution happens in Node.js and how to fix it

A critical prototype pollution vulnerability was discovered in `worker/import-core.js`, where `request.json()` parsed untrusted HTTP request bodies without filtering dangerous keys like `__proto__` and `constructor`. An attacker could send a crafted JSON payload to corrupt the global `Object` prototype, potentially affecting every object in the application runtime. The fix replaces the unsafe parse with a JSON reviver function that strips these dangerous keys before any object is constructed.

critical

How Information Disclosure via Malformed Cache-Control Directives Happens in Node.js and How to Fix It

A critical vulnerability (CVE-2026-13697) was discovered in the undici HTTP client library, allowing attackers to exploit malformed Cache-Control directives for information disclosure and denial of service. This fix upgrades undici from version 7.25.0 to 7.29.0 using npm overrides to ensure all nested dependencies receive the patched version.