💡 Full working example available on GitHub: skip-external-resources-when-signing-dotnet

Introduction

A Word document can contain a picture that is not in the file. The document holds an address, and whatever opens it fetches that address. On a desktop this is a feature - the image updates when the source does. On a server that accepts uploads, it means the person who sent you the file decides which URLs your infrastructure requests.

Safe document loading is a GroupDocs.Signature behaviour for .NET that declines to make those requests. From version 26.9, LoadOptions.SkipExternalResources defaults to true. This article compares the three load modes against the same document, shows how to allow one host without allowing all of them, and covers why signing an untrusted file needs no network access at all.

Why This Matters More Than It Sounds

The attack has a name - server-side request forgery - and three concrete shapes.

An internal address that is unreachable from the internet is reachable from your server, so a crafted document can make your service fetch http://169.254.169.254/ or an admin endpoint on localhost and, depending on what you do with the result, leak it. A UNC path in a document can prompt a Windows host to authenticate outbound, handing credentials to an attacker-controlled server. And a link to a host that simply never answers ties up the loading thread until it times out, which is a cheap way to exhaust a worker pool.

I had assumed this was a theoretical concern until I watched a test document pull an image through a service that had no business making outbound requests at all. None of this requires a bug in the document library. Following a link is what the format asks for; the question is only whether your server should oblige.

Method 1 - The New Default

No LoadOptions at all:

using var signature = new Signature(sourcePath);
return SavePagePreview(signature, previewPath);

Nothing is fetched. The preview renders an empty placeholder where the linked picture would be, and the PNG is smaller than it would otherwise be. That size difference is the most convenient proof available that no request left the machine.

Which features count as external? Linked pictures rather than embedded ones, INCLUDEPICTURE fields, linked pictures in presentations and spreadsheets, and the images and style sheets an SVG references. Embedded content is untouched - it is already in the file.

Method 2 - Whitelist One Address

Plenty of documents link somewhere legitimate: a company CDN, an internal image server, a template store. Allow that and nothing else:

var loadOptions = new LoadOptions
{
    WhitelistedResources = new List<string> { trustedAddress }
};

using var signature = new Signature(sourcePath, loadOptions);

The matching rule deserves attention. It is a case-insensitive substring test against the resource address, which means a short fragment is dangerous: github matches github.attacker.example/payload.png as readily as the host you intended. Use a scheme, a host and a path - the sample whitelists raw.githubusercontent.com/groupdocs-signature/.

Method 3 - Allow Everything

The pre-26.9 behaviour, still available:

var loadOptions = new LoadOptions { SkipExternalResources = false };

Reasonable for documents your own application produced. One trap worth flagging: the obsolete LoadExternalResources property has the opposite polarity, so SkipExternalResources = false is what replaces LoadExternalResources = true. Copy a value across from the old property and you invert your security posture with no error to tell you.

Comparing the Three: When to Use Each

Mode Best For Key Advantages Limitations
Default (skip) user uploads, e-mail, partner files no outbound request is possible linked pictures render as placeholders
Whitelist documents that link to a host you own keeps legitimate links working substring matching needs a long, specific fragment
Allow all files your own systems generated previews look exactly as before restores the SSRF exposure the default removed

What about signing - does that need the resources?

No, and this is the practical payoff. A QR-code signature is applied with the default load settings and no external resource is requested while the document is loaded, signed or saved:

var options = new QrCodeSignOptions("Approved by GroupDocs.Signature")
{
    EncodeType = QrCodeTypes.QR,
    Left = 400,
    Top = 50,
    Width = 120,
    Height = 120
};

SignResult result = signature.Sign(outputPath, options);

The signed output keeps its link, so a user who opens the document later still sees the picture resolved on their own machine. Skipping is a server-side policy, not an edit to the document - which is what makes it safe to apply to files you are handling on someone else’s behalf.

What Changes When You Upgrade

For most services, nothing visible at first glance, and that is worth stating plainly because a security default that changes behaviour everywhere would not survive an upgrade review. The exception is anywhere a preview or thumbnail used to show a linked picture and now shows a placeholder; that is the change doing its job, and the fix is a whitelist entry if the host is yours, or acceptance if the document came from outside.

The honest way to check is the one the sample uses: render the same document under all three modes and compare the output sizes. If the default and the whitelisted previews are identical in size, nothing was fetched in either case - which usually means the host is unreachable from that machine rather than that the whitelist failed, and the sample prints a hint saying exactly that.

The Preview Helper, Since It Is Not Obvious

Two of the three modes above call a small helper, and it is worth showing because PreviewOptions does not take a path:

var previewOptions = new PreviewOptions(
    pageData => File.Create(previewPath),
    (pageData, pageStream) => pageStream.Dispose())
{
    PreviewFormat = PreviewOptions.PreviewFormats.PNG
};

signature.GeneratePreview(previewOptions);

It takes two stream factories - one to create a stream per page, one to release it. The sample document has a single page, so one file is written; for multi-page input, put the page number in the file name or every page will overwrite the last.

Best Practices

  • Treat anything you did not generate as untrusted, including files from partners with good security postures.
  • Make whitelist fragments long enough to be unambiguous, and review them when a CDN changes.
  • Never set SkipExternalResources from a value that used to be assigned to LoadExternalResources.
  • Verify with output sizes rather than with the setting; a configuration that looks right and a request that did not happen are different claims.

Where This Leaves SVG

Worth calling out separately, because SVG is both a common upload format and a common SSRF vector. An SVG can reference images and style sheets by URL, and those references are external resources under the same rule - skipped by default, whitelistable, restorable. A service that accepts SVG avatars or logos and renders them server-side was exactly the shape of system this change protects.

If your pipeline accepts SVG from users, the default is the setting you want, and the whitelist is for the case where your own templates pull a shared stylesheet from a host you run.

Conclusion

The default flipped so that the risky behaviour needs an explicit decision and the safe one needs nothing. Keep the default for untrusted input, whitelist narrowly where your own hosts are involved, and remember that signing itself never needed the network. Running the sample against one of your own documents takes a minute and tells you, in three file sizes, exactly what your service has been fetching.

Additional Resources