Scan uploads with ClamAV

2 min read Updated 1 day ago

Scan uploads with ClamAV

ClamAV is a virus scanning daemon that keeps your uploads safe. Anything you let your users upload could carry malware, and scanning it before you store it, serve it, or pass it on is the cheapest way to keep it out of your system. ClamAV runs clamd, the ClamAV scanning daemon, and your application talks to it over the clamd TCP protocol on port 3310. It is not a REST API, so your code connects with a plain TCP socket rather than an HTTP client.

You find it in the Custom services card when you Create service on your application. It is restricted to a single instance: this service always runs as exactly one replica, so there is no scaling slider to change.

How it works

ClamAV gives you two ways to scan a file:

  • Stream bytes with INSTREAM. This is the recommended approach. You send the file's contents to the daemon over the socket, and it scans them as they arrive. It works no matter where the file lives, because the bytes come to you first.
  • Scan by path with SCAN. You give the daemon a path on its own storage and it reads the file from there. This only works if clamd can actually read that path. The service does not share your application's storage, so in practice this usually means you have copied the file into a place the scanner can see first.

For most applications INSTREAM is the right choice. You already have the upload bytes in your request, so streaming them to the scanner is straightforward and needs no shared filesystem.

What is injected

When you add a ClamAV service, your application gets three environment variables so it knows where to connect:

  • CLAMAV_HOST is the hostname of the service.
  • CLAMAV_PORT is the port, always 3310.
  • CLAMAV_URL is the two combined as a connection string, like tcp://host:3310.

Connecting from PHP

To scan a file, open a socket to CLAMAV_HOST:CLAMAV_PORT, tell the daemon to enter streaming mode, send the file bytes in length-prefixed chunks, close the stream, and read the verdict. A small helper looks like this:

<?php

function scanWithClamAV(string $fileContents): bool
{
    $socket = @stream_socket_client(
        sprintf('tcp://%s:%s', getenv('CLAMAV_HOST'), getenv('CLAMAV_PORT')),
        $errno,
        $errstr,
        10
    );

    if ($socket === false) {
        throw new RuntimeException("Cannot reach ClamAV: {$errstr}");
    }

    // Enter streaming mode.
    fwrite($socket, "zINSTREAM\0");

    // Send the file in 4-byte length-prefixed chunks.
    $chunkSize = 8192;
    $offset = 0;
    $length = strlen($fileContents);

    while ($offset < $length) {
        $chunk = substr($fileContents, $offset, $chunkSize);
        $offset += strlen($chunk);
        fwrite($socket, pack('N', strlen($chunk)).$chunk);
    }

    // Zero-length chunk ends the stream.
    fwrite($socket, pack('N', 0));

    $response = '';
    while (! feof($socket)) {
        $response .= fgets($socket);
    }

    fclose($socket);

    return str_contains($response, ' OK');
}

Call it with the raw bytes of the upload before you persist it. If it returns false, reject the request rather than storing the file.

A few notes on the protocol: the stream is ended with a chunk of length zero, and the daemon answers with a line ending in OK when the file is clean. A longer response names the virus it found. Read the whole response before you close the socket, because the verdict arrives in that final line.

Storage and signatures

ClamAV keeps a virus signature database on persistent storage mounted at /var/lib/clamav. The database survives restarts, and the image runs freshclam alongside clamd so definitions update automatically and stay current. You do not need to refresh the signatures yourself. The storage is shown in the create flow under storage configuration, and you can resize it from the Update resources panel if your database outgrows the default.

The available versions come from the version selector when you create the service. Pick a recent one unless you have a specific reason to stay on an older release.

Managing the service

  • To change how much memory the scanner gets, open the service and choose Edit resources (or Update resources). Saving restarts the service.
  • Service configuration is where any per-service settings live. Options that are not the headline ones sit under Advanced settings.
  • A restart is quick, but any scan in flight during it is dropped, so release a restart during a quiet moment.

Because the service does not share your application's storage, the SCAN command can only read paths the daemon itself can reach. If you only ever scan bytes you already hold, stick with INSTREAM and you never have to think about where the scanner can read from.