Skip to content

Security: enforce postprocessing/antivirus quarantine on the storage Download path#596

Open
thistehneisen wants to merge 1 commit into
owncloud:mainfrom
thistehneisen:security/av-download-quarantine
Open

Security: enforce postprocessing/antivirus quarantine on the storage Download path#596
thistehneisen wants to merge 1 commit into
owncloud:mainfrom
thistehneisen:security/av-download-quarantine

Conversation

@thistehneisen
Copy link
Copy Markdown

Security: enforce postprocessing/antivirus quarantine in the storage Download path

Summary

The "still being processed / virus scan in progress" quarantine that protects
users from downloading unscanned or infected content is enforced only in
the ocdav HTTP handlers (GET / HEAD / PROPFIND), and even there only via a
separate Stat round-trip performed before the data is fetched. The
authoritative blob-serving layer — Decomposedfs.Download and
Decomposedfs.DownloadRevision — performs no processing-state check at all.

As a result the antivirus / postprocessing gate can be bypassed:

  1. Raceless bypass. Any caller that reaches Download / DownloadRevision
    without going through the ocdav GET stat-gate serves the blob with no
    quarantine check. This includes the /data datagateway endpoint (which is
    Unprotected at the oCIS proxy and calls fs.Download directly) and any
    revision-style reference (ResourceId.OpaqueId containing the revision
    delimiter), which is routed straight into DownloadRevision.
  2. TOCTOU bypass. For the ocdav path, the processing state is read from a
    Stat response and the content is then fetched in a separate
    InitiateFileDownload + data request, with no If-Match/etag binding
    between the two. The reva source already acknowledges this race in
    internal/http/services/owncloud/ocdav/get.go (// TODO we need to send the If-Match etag in the GET to the datagateway to prevent race conditions between stating and reading the file).

The correct fix is to enforce the quarantine at the resource boundary
(Decomposedfs.Download / DownloadRevision), so every consumer (ocdav, the
datagateway /data path, the archiver, app-provider, OCM) is covered and the
TOCTOU is closed.

Affected code

  • pkg/storage/utils/decomposedfs/decomposedfs.goDownload()
    resolves the node, checks per-user permissions, then calls
    fs.tp.ReadBlob(n) with no check of the node's postprocessing/scan
    status.
  • pkg/storage/utils/decomposedfs/revisions.goDownloadRevision()
    has the same gap.
  • internal/http/services/owncloud/ocdav/get.go — the only place the
    status == "processing" check exists, performed on a prior Stat result
    (separate request → TOCTOU; explicit TODO acknowledging the race).
  • Downstream context in oCIS: services/proxy/pkg/config/defaults/defaultconfig.go
    routes /data with Unprotected: true; the datagateway authorises with the
    transfer token only and calls fs.Download with no processing gate
    (pkg/rhttp/datatx/utils/download/download.go).

Impact

  • Confidentiality / Integrity: an uploaded file can be retrieved before the
    configured antivirus / postprocessing pipeline has finished or while it is
    reporting "processing", defeating the malware-quarantine guarantee. Combined
    with a public link on the parent folder, distribution is unauthenticated.
  • Prerequisites: any user able to upload a file (the revision//data
    variants are raceless; the ocdav variant requires winning a short, known
    window). No administrative privileges required.
  • Deployments that rely on the antivirus postprocessing step as a security
    control are affected. (Note: in a default oCIS install the antivirus and
    policies services are opt-in and the postprocessing pipeline is empty — that
    is a separate hardening concern tracked elsewhere; this PR fixes the control
    for deployments that do enable it.)
  • Suggested severity: High / Critical depending on deployment.
    CVSS:3.1 (proposed) AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N.

Proposed fix

Enforce the postprocessing/scan quarantine in Decomposedfs.Download and
Decomposedfs.DownloadRevision, before ReadBlob:

  1. After the node is resolved and permissions are assembled, read the same
    postprocessing status that node.AsResourceInfo uses to annotate
    status: processing on the resource (the node scan/processing marker).
  2. If the node is still in postprocessing, return a clear error
    (errtypes.NotFound / a dedicated "still processing" error consistent with
    the existing ocdav 425 Too Early behaviour) instead of streaming the blob.
  3. Verify the antivirus/postprocessing flows do not rely on
    Decomposedfs.Download to read the content under scan — the scanner streams
    the upload .bin via the upload/TUS token path, not via Download, so the
    new gate must not regress scanning. Add an explicit allowance only if a
    legitimate internal-scanner code path is identified during review.

This makes the resource layer (not one HTTP caller) the authoritative
enforcement point and closes both the raceless and TOCTOU variants in a single
place. The ocdav Stat-based pre-check can remain as a fast-path / nicer
status code; the storage-layer check is the security boundary.

Test plan

  • Unit: Download / DownloadRevision return the processing error when the
    node is marked processing; succeed once the marker is cleared.
  • Integration: upload a file with an artificially long postprocessing step;
    assert GET /data..., a revision-reference download, and ocdav GET all
    return the quarantine error during processing and succeed afterwards.
  • Regression: antivirus scan of an uploaded file still completes (scanner path
    unaffected); normal download after a clean verdict works.
  • Race: concurrent processing-finish + download no longer yields the blob
    before the verdict.

Nils Putnins / OffSeq Cybersecurity
npu@offseq.com / https://offseq.com / https://radar.offseq.com

The processing/antivirus quarantine was only enforced by the ocdav
GET/HEAD/PROPFIND handlers via a separate Stat call. Decomposedfs.Download
and DownloadRevision did not check the node's processing state, so the
datagateway /data endpoint and revision downloads could serve
unscanned/in-process content and the ocdav path was racy (TOCTOU).

- reject Download/DownloadRevision with errtypes.TooEarly while the node
  IsProcessing, enforcing the quarantine at the storage layer for every
  caller
- map the too-early error to HTTP 425 in the data transfer handler
@thistehneisen thistehneisen force-pushed the security/av-download-quarantine branch from 62c8394 to c1cc13b Compare May 15, 2026 19:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant