Security

Files.ReadWrite.All: the SharePoint exfiltration that leaves no useful trace

Case study: bounding a Graph permission that reads the entire tenant-wide SharePoint surface.

GraphWarden Team·April 22, 2026·9 min read

The scenario

Saturday, 11 PM. A contract management application, deployed two years ago at a 400-employee engineering firm, starts reading SharePoint. A lot of reading.

In six hours, 240 GB of data leaves the tenant: client contracts, expert reports, bid files, vendor quotes, legal correspondence. Everything that lives in SharePoint libraries, without distinction.

Monday morning, the security team gets a Purview DLP alert: an unusual read volume has been detected. The attack ended 36 hours earlier. The vendor's key had been compromised the previous Thursday. From Thursday to Saturday, reconnaissance. From Saturday to Sunday morning, extraction.

From Microsoft Graph's perspective, everything was compliant: the app had Files.ReadWrite.All, the token was valid, the queries were well-formed. SharePoint audit captured everything, which is exactly where the 240 GB had already left by the time the alert fired.

Why Microsoft doesn't block this scenario

Files.ReadWrite.All as an application permission grants access to every library on every site in the tenant. There is no native scoping equivalent to Sites.Selected for this permission.

Microsoft offers three defensive layers: Sensitivity Labels for at-rest encryption, Purview DLP for exfiltration pattern detection, and unified audit for post-incident reconstruction. Each has its role, none is preventive in real time at the Graph call layer.

  • Sensitivity Labels protect already-labeled files. A mislabeled library passes through.
  • Purview DLP detects patterns after a certain volume. The alert arrives in minutes, the extraction in seconds.
  • Unified audit is retrospective by design. It documents, it does not block.
  • Sites.Selected exists and scopes to specific sites, but requires an application rewrite to adopt the per-site API, which most ISVs will not do.

Preventive governance of Graph calls remains the responsibility of the ecosystem. Same conclusion as Mail.Send: a switch on Microsoft's side, no application firewall.

What the incident looks like with a perimeter in place

Same scenario, but this time all /sites/* and /drives/* calls are routed through a governance proxy. Every request is evaluated before reaching Graph.

Baseline: default-deny. Any call that matches no allow rule is blocked. The rules that follow define what the contract management app actually needs to access.

Rule 1 — Which sites, which libraries?

- name: "Contracts — site scope"
  endpoint_pattern: "/sites/*/drives/*"
  http_methods: [GET]
  conditions:
    all_of:
      - caller_identity:
          application_id: "app-contract-mgmt"
      - object_attribute:
          path: "siteId"
          in: ["site-contracts-active", "site-contracts-archive"]
  action: allow

The application can read two sites, the ones actually containing contracts. HR, finance, executive, and confidential project sites stay inaccessible, even though Files.ReadWrite.All is granted at the Graph level.

Rule 2 — Bandwidth-bound, not just request-bound

- name: "Contracts — bandwidth ceiling"
  endpoint_pattern: "/sites/*/drives/*/items/*/content"
  conditions:
    all_of:
      - bandwidth_limit:
          max_bytes: 5368709120   # 5 GB
          window_seconds: 86400
          scope: "caller_identity"
  action: allow

Most rate limiters count requests. For exfiltration, the signal is volume. 5 GB per day is plenty for normal contract management. Beyond that, the app is cut off before 240 GB can leave.

Rule 3 — Allowed file types

- name: "Contracts — document types only"
  endpoint_pattern: "/sites/*/drives/*/items/*/content"
  conditions:
    all_of:
      - file_extension:
          in: [".docx", ".pdf", ".xlsx"]
  action: allow

The application needs contracts and attachments, not 2 GB ZIP archives nor .pst files. Filtering on extensions blocks a classic exfiltration pattern where an attacker archives a whole site before draining it.

Rule 4 — Sensitivity Label as a blocking signal

- name: "Contracts — block confidential labels"
  endpoint_pattern: "/sites/*/drives/*/items/*"
  conditions:
    any_of:
      - object_attribute:
          path: "sensitivityLabel.displayName"
          in: ["Confidential-Legal", "Confidential-HR", "Restricted"]
  action: block

Microsoft Purview applies the labels. GraphWarden reads them back before serving the request. A file tagged Confidential-HR in a site the app would otherwise be allowed to read is still refused at read time. Defense in depth moves from two layers to three, applied in real time.

Rule 5 — Enumeration detection

- name: "Contracts — enumeration trap"
  endpoint_pattern: "/sites/*/drives/*/items"
  conditions:
    all_of:
      - rate_limit:
          max: 500
          window_seconds: 3600
          scope: "caller_identity"
      - pattern_detect:
          signal: "sequential_pagination"
          threshold: 20
  action: block

A massive extraction always starts with recursive enumeration of libraries. Paginating 20 times in a row without accessing content is a strong signal. Preventive blocking cuts reconnaissance before extraction begins.

The bandwidth_limit, pattern_detect, and file_extension conditions are part of the rule grammar. See the documentation for the full list.

See the rule documentation →

What changes for the decision-maker

SharePoint exfiltration via a compromised application is the case where the difference between detection and prevention is directly quantifiable. Purview DLP detects after the fact. Sensitivity Labels protect labeled files. Neither cuts the connection mid-extraction.

With a governance layer in front of Graph, three shifts happen.

  1. Effective scope matches actual scope. Files.ReadWrite.All stays granted at the Graph level, but the proxy narrows it to the two sites the app actually uses.
  2. Defense becomes synchronous. A refused call does not go out. The 240 GB never leaves the tenant, even if the key is compromised.
  3. Audit is vendor-independent. Every request, allowed or refused, generates an event outside the Microsoft tenant. In an investigation, evidence does not wait for SharePoint log reconstruction.

The limits worth naming

The site-scope rule assumes you know the legitimate sites. For a multi-tenant ISV, the list has to be maintained per customer and synchronized with the site inventory. That work already existed in any serious Sensitivity Labels governance effort. It is simply brought forward.

The Sensitivity Label filter presupposes libraries are correctly labeled. If a library contains confidential documents without a label, rule 4 does not protect them. Rules 1, 2, 3, and 5 remain effective. That is the point of defense in depth.

Last limit: the application must go through the proxy. If it retains direct Graph credentials, it bypasses the layer. The correct trust model keeps the real Graph secret in the customer's vault and issues the application a proxy credential, useless anywhere else.

The takeaway

Files.ReadWrite.All is the permission most requested by applications that touch SharePoint, and the one whose risk is most often misread. The promise "we have DLP" is an incomplete truth: DLP detects, it does not block Graph calls in real time.

For any organization whose SharePoint data is worth something (professional firms, financial institutions, healthcare, public sector), bounding this permission with a declarative governance layer turns an unbounded risk into a documented, auditable, and cuttable one.

That is exactly why GraphWarden exists.

GraphWarden is built in Quebec, hosted in Canadian regions, and designed for environments where privacy law compliance and Microsoft Graph application control are non-negotiable requirements.