rekhu

Seshat

File safety

How Seshat checks every file for safety before it works on it, what the safety report says, and how your app handles an unsafe file.

What this is for

Your customers send you files, and you cannot know what is in them. Before Seshat works on a file, it checks it: for viruses, for what the file really is, and for anything in it that could act on its own when someone opens it. A file that is not safe is refused and never kept. A file with parts a print does not need is taken, and those parts can be removed before printing.

There is nothing to switch on. Every file a customer sends goes through the check: with check, upload, uploadInParts, enhance, fillTemplate and textPositions. Files Seshat made itself, such as a fixed PDF, are not checked again.

What is checked

  • Viruses. Every file is scanned, with signatures updated every day.
  • What the file really is. Seshat reads the first bytes, not the name. A program called flyer.pdf is refused, and so is a file whose name says PDF while it is a picture or a ZIP.
  • What could act on its own when the PDF is opened: scripts, actions that start a program, programs or other files embedded in the PDF, forms that send data to an address, an address opened as soon as the file opens, and sound, video or 3D.
  • Files built to do harm. A picture built to exhaust memory rather than to be printed, or a PDF with far more objects than a print file needs, is refused.
  • What is worth knowing. A locked PDF, or a page larger than five metres, is noted.

The check reads the file from storage as a stream, so a file of gigabytes is checked without being held in memory.

The safety report

The answer of every call that takes a file carries the report as safety:

$check = Rekhu::seshat()->check($request->file('artwork'), $order);

$check['safety'];
// ['verdict' => 'remove', 'virus' => ['status' => 'clean'], 'kind' => 'pdf', 'findings' => [
//     ['code' => 'scripts', 'decision' => 'remove', 'about' => 'the PDF carries scripts that run in a viewer']]]
const check = await seshat.check(buffer, { name: 'flyer.pdf', order });

check.safety;
// { verdict: 'remove', virus: { status: 'clean' }, kind: 'pdf', findings: [
//     { code: 'scripts', decision: 'remove', about: 'the PDF carries scripts that run in a viewer' }] }
Field
verdict clean, remove or refuse
virus status, and signature when a virus was found
kind what the file really is: pdf, png, jpeg, tiff, webp, gif, eps, zip
findings each with code, decision (refuse, remove or note) and about; some name the files inside

The three verdicts

Verdict Meaning What your app does
clean nothing found go on
remove the file is taken; it carries parts a print does not need go on, and run the clean_up fix before printing
refuse the file is not taken and not kept tell the customer why, and ask for another file

The findings behind each decision:

Decision Codes
refuse program, not_what_it_says, embedded_program, launch, picture_bomb, too_many_objects, unreadable
remove scripts, xfa_form, form_sends, media, opens_address, attachments
note locked, huge_page

Clean up a file

A file with the verdict remove can be printed once those parts are out. The clean_up fix draws form fields and comments into the page as they show, and removes hidden layers, scripts, attachments and links. With the option remove, form fields and comments are left out instead of drawn.

$fixed = Rekhu::seshat()->fix($check['file_id'], [['fix' => 'clean_up']], $order);
const fixed = await seshat.fix(check.file_id, [{ fix: 'clean_up' }], order);

applied in the answer says what was found and removed. Combine clean_up with other fixes in the same call: it is still one charge.

The safety in a fix answer describes the file the fix made: after clean_up its verdict is clean. When a file cannot be inspected for safety, it is not taken; the call answers that it could not be checked just now, and sending it again checks it.

Handle an unsafe file

A refused file throws UnsafeFile: status 422, type refused_unsafe. The message says why, and safety holds the report when Rekhu gave it. Nothing is charged.

  1. Catch UnsafeFile around the call that sends the customer's file.
  2. Tell the customer why. The message names the reasons, such as "the file is called .pdf but is zip".
  3. Keep the report with the order, if you log refused files.
  4. Ask for a new file. Do not send the same file again: it is refused again.
use Rekhu\Core\Exceptions\UnsafeFile;
use Rekhu\Core\Rekhu;

try {
    $check = Rekhu::seshat()->for($user)->check($request->file('artwork'), $order);
} catch (UnsafeFile $e) {
    Log::warning('Artwork refused', ['user' => $user->id, 'safety' => $e->safety]);

    return back()->withErrors(['artwork' => $e->getMessage()]);
}
import { UnsafeFile } from '@rekhu/core';

try {
    const check = await rekhu.seshat({ user }).check(buffer, { name: 'flyer.pdf', order });
    res.json(check);
} catch (error) {
    if (!(error instanceof UnsafeFile)) throw error;
    logger.warn('Artwork refused', { user: user.id, safety: error.safety });
    res.status(422).json({ message: error.message });
}

UnsafeFile extends RekhuException (Laravel) and RekhuError (Node.js), so a catch of the base class still catches it. See Errors.

Note: the safety report says whether a file is safe to work on, not whether it prints well. Whether it prints well is what the check's own findings say; see Checking files.

Next

  • Large files: the safety check of a file sent in parts.
  • Errors: every error type in both packages.