Skip to main content
Jahia Store
EN

ClamAV filter

community
Download 1.1.0

Information

Module ID
clamav-filter
Group ID
org.jahia.community
Status
community
Category
Security
Author
Florent BOURASSE
Developer website
http://www.jahia.com
Requires Jahia
8.2.3.2
Released
2026-09-21
Source
scm:git:git@github.com:Jahia/clamav-filter.git
Tags
  • security

A Jahia module that scans uploaded files against a ClamAV antivirus daemon and blocks any upload containing detected malware.

Screenshots

Dependencies

Depended on by

Nothing depends on this module.

Versions

Security release closing the JAHIA-SEC-418 scan-coverage residual (medium, CVSS 4.3, CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N). Everyone running 1.0.4 or earlier should upgrade.

The minor version bump — rather than 1.0.5 — is deliberate: the public surface (GraphQL API, OSGi configuration keys, the ClamavService interface) is unchanged, but runtime behaviour is not. Read the breaking-change section before upgrading.

The vulnerability

The 1.0.4 fix (SEC-141) generalised scanning from multipart-only to an allow-list of exactly two further request shapes — a body whose Content-Type starts with application/octet-stream, and a PUT carrying a body. 

Any authenticated user who could reach an upload channel could choose a bypass with nothing more than a header value or an HTTP verb. Reproduced live on 8.2.3.2 and 8.2.4.0-SNAPSHOT.

The fix

The scan gate is now deny-by-default: any request carrying a body is buffered and scanned through the same fail-closed path, whatever the method and whatever the declared content type, unless its media type is explicitly exempt.

  • Media types are compared case-insensitively with parameters stripped; the HTTP method no longer decides whether an upload is scanned.
  • An unknown Content-Length is treated as a body unless the method is GET/HEAD, rather than keying on Transfer-Encoding — RFC 9113 §8.2.2 forbids that header in HTTP/2, so an h2 streamed upload carries neither and would otherwise pass unscanned.
  • A request whose body turns out to be empty is forwarded without contacting the daemon.

⚠️ Breaking change — read before upgrading

The set of scanned requests is much larger.

You are impacted if your deployment sends non-multipart, non-octet-stream bodies through Jahia — PATCH/DELETE bodies, text/*, application/xml, image or document content types. These were never scanned before and now are, so they come under the module's existing fail-closed behaviour: an infected body gets 403, and while the ClamAV daemon is unreachable they get 503 instead of passing through. The multipart provisioning API (/modules/api/provisioning) has been scanned since 1.0.4, so module deployment through it also fails while ClamAV is down.

You are not impacted on application/json (and any +json), application/x-www-form-urlencoded, application/graphql or text/x-gwt-rpc. These are exempt, so GraphQL, REST, login and the GWT-RPC authoring UI (jContent, Content Manager, Page Composer) behave exactly as before during a ClamAV outage. GraphQL file uploads use multipart and were already scanned. Bodyless requests, GET and HEAD are untouched.

To upgrade: make sure the ClamAV daemon is reachable and sized for the added traffic before deploying. No configuration change is required — the exemption list is intentionally not configurable, so it cannot be widened into a bypass at runtime.

Verification

  • 176 unit tests (was 129). Mutation-checked: restoring the pre-SEC-418 predicate fails 20 of them, so they detect the vulnerability rather than merely describe the code.
  • Live A/B using the advisory's own reproduction and its own discriminator (the fail-closed log-line delta, never the HTTP status — every bypassed arm answers 400/404/405 from the application, which is indistinguishable from a block if you read status codes). On 8.2.3.2 with clamd deliberately unreachable: all three positive controls fired, and all five shapes previously measured as unscanned on 1.0.4 are now scanned. Exemptions and bodyless requests remain unscanned.
  • Docker Cypress suite: 33/33, all five specs green against a live ClamAV daemon.
  • OSGi Import-Package unchanged from 1.0.4.

Also in this release

  • Build fix: the pinned Node was raised v22.6.0 → v22.11.0. The module could not be built at all — the locked sass-loader@17.0.0 requires Node >= 22.11.0, and Yarn Classic treats the engine mismatch as fatal, so yarn install aborted before webpack ran.
  • Test harness: cy.apollo() failed suite-wide with Failed to execute 'fetch' on 'Window': Illegal invocation, because cross-fetch's browser build re-exports the native fetch detached from the window. Worked around locally in cypress/support/bindFetch.js; the proper fix belongs upstream in Jahia/jahia-cypress.
  • Test provisioning now configures the daemon host before any spec runs, and ci.startup.sh forwards its arguments so the documented ./ci.startup.sh notests works.

What's Changed

  • chore: post-1.0.4 cleanup — AGENTS.md GraphQL accuracy + six code findings by @fbourasse in #45
  • build(deps): triage the 13 Dependabot PRs — accept build/test scope, hold platform-coupled by @fbourasse in #46
  • build(deps): triage round-2 Dependabot PRs — accept 9, hold babel-jest and moonstone by @fbourasse in #58
  • chore(tests): make tsc --noEmit clean and add a typecheck script by @fbourasse in #59
  • fix: make the scan gate deny-by-default (SEC-418 scan-coverage residual) by @fbourasse in #77

Full Changelog: 1_0_4...1_1_0

Requires Jahia 8.2.3.2Released 2026-09-21

⚠️ Breaking change — GraphQL operations are now namespaced

The flat root fields were replaced by a single clamav namespace container on both Query and Mutation. Any client using the old field names must be updated.

Before (1.0.1)

Now (1.0.4)

clamavSettings

clamav { settings }

clamavPing

clamav { ping }

clamavScanTest(content:)

clamav { scanTest(content:) }

clamavSaveSettings(...)

clamav { saveSettings(...) }

query { clamav { settings { host port connectionTimeout readTimeout } ping } }

 

Flat root fields collide across modules — two bundles registering the same global field makes the entire GraphQL schema fail to build. Grouping under one container per module removes that whole class of failure and makes ownership explicit.

Security

  • Raw binary upload coverage (SEC-141). Scanning previously covered only multipart/form-data and the Forms octet-stream endpoint, so malware delivered over a non-multipart channel entered the repository unscanned. Any application/octet-stream body and any PUT carrying a body — WebDAV and raw-body JCR REST binary writes — now go through the same fail-closed path.
  • No attacker-toggleable bypass. A webflowToken-based skip was removed: a guard the uploader can flip is not a guard. Scanning is never conditional on a client-supplied value.
  • No TOCTOU gap. The buffered request wrapper is forwarded downstream, so the bytes scanned are exactly the bytes Jahia consumes.
  • Every multipart part is scanned, including multiple files posted under the same field name (previously collapsed to one, leaving the rest unscanned but replayed downstream).
  • Fail-closed. Scanner unreachable or a scanner error rejects the upload with 503; uploads are never silently passed through.
  • Fine-grained RBAC. A module-specific clamavAdmin permission gates the settings and test endpoints, plus an assignable clamav-filter-administrator role granting only administrationAccess + clamavAdmin — so this capability can be delegated without granting full server admin.
  • Host validation on every trust boundary, including configs pushed straight through ConfigurationAdmin (a .cfg file), not just the admin UI.
  • Heap-DoS ceiling. Bodies above 100 MiB are rejected with 413 before buffering; bounded daemon replies and CRLF-sanitised log output close log-injection and unbounded-read paths.
  • Early mid-stream detections are classified as real detections rather than swallowed as generic errors.

Response codes

Outcome

Status

Clean

passes downstream (wrapped request)

Infected

403 Forbidden

Body over 100 MiB

413 Payload Too Large

Scanner unreachable / error

503 Service Unavailable (fail-closed)

Unexpected parse/read failure

500 Internal Server Error

Quality

  • 133 JUnit tests plus 36 Jest tests and Cypress e2e coverage for the fail-closed core, GraphQL resolvers, permission enforcement, raw binary uploads and schema namespacing.
  • Atomic OSGi config validation — an invalid update is rejected wholesale and leaves the running config untouched.
  • Result is now an immutable value object; admin UI audited for WCAG 2.2 AAA.

Dependencies

All Dependabot alerts resolved across both Yarn workspaces (26 in this cycle, plus earlier tmp, qs, uuid and assertj-core fixes). Every one is a build-time or test-time dependency — none ships inside the OSGi bundle.

.github/dependabot.yml had shipped as the GitHub scaffold with an invalid empty package-ecosystem, so version updates never ran and routine patches only ever surfaced later as security alerts. Now configured for npm (/ and /tests) and maven, with grouped updates.

Documentation

Corrected upload-coverage claims that had been stale since SEC-141: the README still stated WebDAV PUT and raw-body JCR REST uploads were not intercepted and told operators to defend them separately — the opposite of current behaviour.

Note on versioning: 1.0.2 and 1.0.3 were bumped in pom.xml but never tagged, so this release follows 1_0_1 directly.

Full changelog: 1_0_1...1_0_4

Requires Jahia 8.2.1.0Released 2026-09-14

Highlights

  • Security hardening of the ClamAV filter pipeline: bounded request buffering, fail-closed on scanner unavailability, SSRF input validation, and TOCTOU mitigation.
  • Full WCAG 2.1 AA audit remediation plus push to AAA 7:1 contrast across the admin UI.
  • Dependency vulnerability fixes (Dependabot/CVE) on @babel/plugin-transform-modules-systemjs and systeminformation.
  • Project guidance docs (AGENTS.md / CLAUDE.md) added for AI-assisted development.

Fixes

Security (431fe9d)

  • MultiReadHttpServletRequest: bound buffered body by configurable max size; throws RequestTooLargeException → HTTP 413 to prevent unauthenticated heap DoS.
  • ClamavFilter: scoped to scan-eligible requests, forwards the wrapped request downstream so scanned bytes equal consumed bytes (closes TOCTOU); fails-closed with HTTP 503 when ClamAV is unreachable instead of silently passing uploads.
  • clamavSaveSettings mutation: validates host charset/length, port range and timeout bounds to block scanner-rewrite SSRF that could bypass scanning.
  • clamavScanTest: caps base64 input length to prevent decode DoS.
  • ClamavServiceImpl: bounded INSTREAM reply reads, CRLF-stripped/truncated log messages, explicit US-ASCII/UTF-8, defensive bounds checks in populateVirusScanResult.
  • ClamavConfig: atomic full-update validation; fields made volatile.

Accessibility — WCAG 2.1 AA (ec95a77, c275ea8)

  • aria-hidden="true" on alert icon spans (✓ / ✕) to stop AT reading Unicode names.
  • aria-live="polite" on filename display; kept always in DOM so selection is announced.
  • Replaced 3 dynamic-role live regions with 6 fixed alert+status pairs (save / ping / scan) for stable AT subscriptions.
  • Removed focus() calls on sr-only divs after async ops; aria-live announces without stealing focus.
  • 4 px left accent border on alert boxes for boundary contrast.
  • aria-describedby host-input format hint; tooltip colour darkened #767676 → #6b6b6b.
  • aria-describedby + sr-only rationale on disabled scan section.

Accessibility — WCAG 2 AAA 7:1 contrast (0833b6b, 3979225)

  • Alert success text: #2e7d32 → #1b5e20 → #175217 (final ~8.3:1 on #e8f5e9).
  • Alert error text: #c62828 → #9e1a1a (~7.1:1 on #fdecea).
  • Buttons/labels: #0077cc → #004a80 (~9.2:1 on white).
  • Input focus borders/outlines aligned for consistency.

Dependency / CVE

  • 2229fbc — upgrade systeminformation 5.31.5 → 5.31.6 via resolution (CVE fix).
  • c5c9a42 — upgrade @babel/plugin-transform-modules-systemjs (Dependabot alert).

Refactoring

  • ClamavFilter / MultiReadHttpServletRequest / ClamavServiceImpl / ClamavConfig substantially reworked alongside the security fix (431fe9d) — clearer scoping, explicit charsets, atomic config swaps.
  • ClamavConstants extended with new bounds/error constants.
  • GraphQL extensions (ClamavMutationExtension, ClamavQueryExtension) reorganised around input validation and error reporting.
  • UI components (ClamavFilter.jsx / .scss) restructured around fixed live-region pairs and contrast-compliant tokens.
  • Locale en.json updated for new validation and accessibility messages.

Tests

  • tests/package.json and tests/yarn.lock updated (dependency bumps); no new test commits identified in range.

Docs / Tooling

  • cba8b9d — add Claude basic context.
  • 38113e3 — rename CLAUDE.md to AGENTS.md, add CLAUDE.md reference shim.

Full Changelog: 1_0_0...1_0_1

Requires Jahia 8.2.1.0Released 2026-05-19
Requires Jahia 8.2.1.0Released 2026-04-18