/* Copyright 2026 aXion1337 SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE files in the repository root for full details. */ // Client-side ClamAV scanning (Issue #19 extension): Synapse's own server-side spam-checker // module can never see E2EE attachment content - only a cooperating client can, since only // the client ever holds the room's decryption key. This calls a small self-hosted scan // service (same ClamAV instance the server-side module uses) directly from the browser, both // before encrypting/uploading a file and after downloading/decrypting one - see // ContentMessages.ts (send) and DecryptFile.ts (receive) for the two call sites. import { logger } from "matrix-js-sdk/src/logger"; export class ContentScanRejectedError extends Error { public readonly signature: string; public constructor(signature: string) { super(`Blocked by content scanner: ${signature}`); this.name = "ContentScanRejectedError"; this.signature = signature; } } /** * Scans plaintext bytes against the self-hosted ClamAV scan service. * Fails open (resolves normally) on any network/scanner-side error, matching the same * fail-open policy as the server-side Synapse module - a scanner outage should not block * uploads or downloads site-wide. * @throws {ContentScanRejectedError} if the scanner positively identifies the content as infected. */ export async function scanContent(data: ArrayBuffer | ArrayBufferView, accessToken: string): Promise { let response: Response; try { response = await fetch("/_scan", { method: "POST", headers: { Authorization: `Bearer ${accessToken}` }, body: data as BodyInit, }); } catch (e) { logger.warn("Content scan request failed (scanner unreachable?) - allowing through", e); return; } if (!response.ok) { logger.warn("Content scan request failed with status", response.status, "- allowing through"); return; } let result: { clean: boolean; signature?: string }; try { result = await response.json(); } catch (e) { logger.warn("Content scan response was not valid JSON - allowing through", e); return; } if (result.clean === false) { throw new ContentScanRejectedError(result.signature ?? "unknown"); } }