onedrive: download malware-flagged files via Graph Prefer header

When --onedrive-av-override is set and the object is malware-flagged,
download via Microsoft Graph beta contentStream then /content with
Prefer: forceInfectedDownload, and keep Prefer (and AVOverride) on the
SharePoint redirect without re-encoding tempauth.

Clean files keep using the stable v1.0 /content path so permanently
enabled av_override does not put all traffic on beta APIs.

AI-assisted change; tested against OneDrive for Business with
application permissions (client_credentials).
This commit is contained in:
ifloppy
2026-07-28 17:33:55 +01:00
committed by Nick Craig-Wood
parent 9a0d7e57dd
commit 512ed643f7
+108 -3
View File
@@ -393,6 +393,16 @@ In this case you will see a message like this
If you are 100% sure you want to download this file anyway then use
the --onedrive-av-override flag, or av_override = true in the config
file.
When set, malware-flagged files are downloaded via Microsoft Graph
beta APIs with Prefer: forceInfectedDownload (contentStream, then
/content). Clean files continue to use the stable v1.0 endpoint.
This is a beta API and may change. It works reliably with application
permissions (client_credentials). With delegated (user) login on
OneDrive for Business, Microsoft often still blocks the download.
tenant_url configurations fall back to the legacy AVOverride query
parameter.
`,
Advanced: true,
}, {
@@ -2406,25 +2416,120 @@ func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.Read
}
fs.FixRangeOption(options, o.size)
var resp *http.Response
// Only malware-flagged files use Graph beta; clean files stay on stable v1.0.
if o.fs.opt.AVOverride && o.fs.opt.TenantURL == "" && o.malwareDetected() {
return o.openInfected(ctx, options...)
}
opts := o.fs.newOptsCall(o.id, "GET", "/content")
opts.Options = options
if o.fs.opt.AVOverride {
// SharePoint v2 (tenant_url) or non-flagged objects: keep legacy query.
opts.Parameters = url.Values{"AVOverride": {"1"}}
}
return o.openWithRedirect(ctx, &opts)
}
// malwareDetected reports whether metadata says this object is malware-flagged.
func (o *Object) malwareDetected() bool {
return o.meta != nil && o.meta.malwareDetected
}
// openInfected downloads a malware-flagged file using Graph beta APIs.
// contentStream applies Prefer for the whole transfer (needs application auth on many tenants);
// beta /content + Prefer is tried next.
func (o *Object) openInfected(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) {
in, err = o.openContentStream(ctx, options...)
if err == nil {
return in, nil
}
fs.Debugf(o, "contentStream download failed, trying beta /content: %v", err)
in, err2 := o.openContentPrefer(ctx, options...)
if err2 == nil {
return in, nil
}
return nil, fmt.Errorf("%w; beta /content also failed: %v (malware download often requires application permissions / client_credentials, or a tenant admin account)", err, err2)
}
// openContentStream streams the object via Graph beta contentStream.
func (o *Object) openContentStream(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) {
var resp *http.Response
id, drive, _ := o.fs.parseNormalizedID(o.id)
if drive == "" {
drive = o.fs.driveID
}
opts := rest.Opts{
Method: "GET",
RootURL: graphAPIEndpoint[o.fs.opt.Region] + "/beta/drives/" + drive,
Path: "/items/" + id + "/contentStream",
Options: options,
ExtraHeaders: map[string]string{
"Prefer": "forceInfectedDownload",
},
}
err = o.fs.pacer.Call(func() (bool, error) {
resp, err = o.fs.srv.Call(ctx, &opts)
return shouldRetry(ctx, resp, err)
})
if err != nil {
return nil, err
}
if resp.StatusCode == http.StatusOK && resp.ContentLength > 0 && resp.Header.Get("Content-Range") == "" {
o.size = resp.ContentLength
}
return resp.Body, nil
}
// openContentPrefer downloads via Graph beta /content with Prefer: forceInfectedDownload.
func (o *Object) openContentPrefer(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) {
id, drive, _ := o.fs.parseNormalizedID(o.id)
if drive == "" {
drive = o.fs.driveID
}
opts := rest.Opts{
Method: "GET",
RootURL: graphAPIEndpoint[o.fs.opt.Region] + "/beta/drives/" + drive,
Path: "/items/" + id + "/content",
Options: options,
ExtraHeaders: map[string]string{
"Prefer": "forceInfectedDownload",
},
}
return o.openWithRedirect(ctx, &opts)
}
// openWithRedirect downloads via /content style endpoints that 302 to a preauthenticated URL.
func (o *Object) openWithRedirect(ctx context.Context, opts *rest.Opts) (in io.ReadCloser, err error) {
var resp *http.Response
// Make a note of the redirect target as we need to call it without Auth
var redirectReq *http.Request
opts.CheckRedirect = func(req *http.Request, via []*http.Request) error {
if len(via) >= 10 {
return errors.New("stopped after 10 redirects")
}
req.Header.Del("Authorization") // remove Auth header
// Preauthenticated download URLs must not carry the Graph Authorization header.
req.Header.Del("Authorization")
// Keep Prefer on the redirect when forcing infected download; some SharePoint
// endpoints honor it only on the final download request.
// Do not delete Prefer here: users may set it via --header.
if o.fs.opt.AVOverride {
if req.Header.Get("Prefer") == "" {
req.Header.Set("Prefer", "forceInfectedDownload")
}
// Append AVOverride without re-encoding tempauth (re-encoding breaks the signature).
if !strings.Contains(req.URL.RawQuery, "AVOverride=") {
if req.URL.RawQuery == "" {
req.URL.RawQuery = "AVOverride=1"
} else {
req.URL.RawQuery += "&AVOverride=1"
}
}
}
redirectReq = req
return http.ErrUseLastResponse
}
err = o.fs.pacer.Call(func() (bool, error) {
resp, err = o.fs.srv.Call(ctx, &opts)
resp, err = o.fs.srv.Call(ctx, opts)
if redirectReq != nil {
// It is a redirect which we are expecting
err = nil