Validating file signature

Prevent file type tampering.

Before ending this section and going to the actual file logic, let's just make a final improvement to validation. Even though we are validating a file's extension, someone may create a malicious file and manually alter its extension, and our system would accept it. Obviously this is not interesting at all. We may mitigate this problem by also validating the file signature (also known as magic number or magic bytes), which corresponds to a file's first bytes, identifying its type. They remain unchanged even if the extension is manually altered, for example.

To begin, let's install the library magic-bytes, which allows to obtain a file's signature.

yarn add magic-bytes.js

What we'll do now is to create a new, custom FileValidator. This time, it will be created from scratch. So, begin by creating the file validators -> file-signature.validator. Here, we should create a class that extends the FileValidator. We then get this class signature:

export class FileSignatureValidator extends FileValidator {}

First, let's implement the method that generates the error message.

buildErrorMessage() {
  return 'Validation failed (file type does not match file signature)';
}

Then, import the magic-bytes library like the following (requires manual importation):

import magicBytes from 'magic-bytes.js';

Now, implement the isValid() method. Here, we perform these steps:

  • The fileSignatures are obtained from the file's buffer (More than one signature may be present, that's why an array is returned)

  • It is checked if any signature is obtained, as some file types don't have one, such as txt files

  • It is verified if the file's signature matches its extension

isValid(file: Express.Multer.File) {
  const fileSignatures = magicBytes(file.buffer).map((file) => file.mime);
  if (!fileSignatures.length) return false;

  const isMatch = fileSignatures.includes(file.mimetype);
  if (!isMatch) return false;

  return true;
}

We have concluded our validator. Go back to the createFileValidators() function and include it in its return.

new FileSignatureValidator()

However, notice that it requires an options object, even if no options will be used. This is due to the FileValidator constructor. To avoid instantiating it like this, we may go back to the file-signature.validator file and define a constructor with no parameters that simply calls the constructor of the superclass while passing this empty options object.

constructor() {
  super({});
}

Excellent, now if the file's extension is accepted, its signature will also be checked, to determine if it matches the extension.

Commit - Validating file signature

Last updated