đź’ˇ Full working example available on GitHub: secure-untrusted-document-upload-conversion-dotnet
The Accounts-Payable Challenge: Why Vendor Upload Conversion Breaks at Scale
Your server converts the invoice; the vendor’s HTML decides what else it reads. Safe vendor-upload conversion with GroupDocs.Conversion for .NET turns an uploaded file into PDF without letting the file’s own references pick what your server fetches or reads next.
A vendor invoice portal lets any approved supplier upload an invoice - usually as HTML or a PDF, sometimes with an attached SVG drawing for a facilities or construction line item - and the accounts-payable system converts it to PDF for archive and approval routing. The vendor wrote that HTML, not your team, and it can reference anything a web page can reference: a stylesheet, a script, an image hosted on the vendor’s own CDN, or - if the vendor’s account was compromised - an address that was never meant to be reachable from your network at all.
Vendor volume only grows. When it goes from a handful of suppliers to hundreds, the uploads folder that once held one company’s files now holds everyone’s - and a relative image reference inside one vendor’s invoice can resolve to a file that belongs to a different vendor sitting right next to it.
What an accounts-payable team actually needs is not a network firewall rule, but a conversion step that treats every reference inside an upload as untrusted by default, lets a reviewed asset like a vendor’s logo through deliberately, and fails loudly - not silently - when a whitelist entry is written wrong.
Reality check: a single
<img>tag pointed athttp://169.254.169.254/latest/meta-data/iam/security-credentials/asks the converting server to call the cloud metadata service - a request the uploader could never make from outside. That is server-side request forgery, and an HTML upload is all it takes.
Why Generic Fixes Don’t Close This Gap
Accounts-payable teams typically reach for one of these before adopting a conversion library with its own resource policy:
- Network egress blocking on the conversion host: stops a remote fetch, but does nothing about a same-folder file a relative reference can resolve to - exactly what happens when an upload is converted straight from its saved path.
- Stripping tags before conversion: fragile against new reference
types (
<link>, SVG<image>, CSS@import) and has to be re-audited every time the upload format changes. - Trusting the vendor portal’s own upload scanner: scans for malware signatures, not for a URL that points at your own infrastructure.
GroupDocs.Conversion closes this gap at the point where it matters most: the moment the document is loaded for conversion, before any output is produced.
The Solution: GroupDocs.Conversion in a Vendor Invoice Workflow
GroupDocs.Conversion for .NET 26.10 denies external resources by default
through SkipExternalResources, lets you name exactly the locations you
trust through WhitelistedResources, and gives you a per-resource callback
through IExternalResourceHandler that can confirm, override, or supply a
resource’s bytes directly - so a reviewed vendor logo can still appear in
the output PDF without a network request ever being made for it.
To follow the implementation below, you’ll need:
- .NET 8.0 or later
- GroupDocs.Conversion for .NET (get a free trial)
- A sample HTML or SVG upload you do not control
dotnet add package GroupDocs.Conversion --version 26.10.0
Implementing Vendor Upload Conversion Step by Step
The example below maps to a real vendor-portal scenario: converting an
uploaded invoice (invoice-upload.html) that references a logo, an
internal address, a local file, and a file sitting next to it.
Most snippets below pass a ResourceAudit object - a small
IExternalResourceHandler that records each load decision and can supply a
trusted asset’s bytes. Its full code is in
Program.cs of the sample repository.
Step 1 - Load and Convert From the Upload’s Saved Path
This is how a portal might convert an upload immediately after saving it to a shared folder - and the step where a sibling file can leak in.
var audit = new ResourceAudit();
var loadOptions = new WebLoadOptions { ExternalResourceHandler = audit };
using (var converter = new Converter(uploadPath, (LoadContext _) => loadOptions))
{
converter.Convert(outputPath, new PdfConvertOptions());
}
return audit.Loaded;
Loading straight from uploadPath puts the invoice’s entire folder in
scope, not just the one file inside it. Scope, not trust, decides what
loads. In this run, every remote and file:/// reference in the invoice
is blocked, but other-customer-signature.svg - a different file saved in
the same folder - loads, because SkipExternalResources only excludes
references outside that folder.
Step 2 - Configure for Vendor-Upload Requirements
For vendor uploads, remove the base folder entirely by loading from a stream instead of a path:
var audit = new ResourceAudit();
var loadOptions = new WebLoadOptions { ExternalResourceHandler = audit };
using (var converter = new Converter(
() => File.OpenRead(uploadPath), (LoadContext _) => loadOptions))
{
converter.Convert(outputPath, new PdfConvertOptions());
}
return audit.Loaded;
Why this setting matters for vendor portals:
- Stream input: a document converted from a stream has no folder, so a relative reference inside it cannot resolve to another vendor’s file.
ExternalResourceHandler: still runs for every resource, so you keep a record of exactly what each vendor upload asked for.
Step 3 - Execute and Capture a Reviewed Asset
A vendor’s logo is one reference you may actually want to keep - supplied from a copy you control, not fetched from the vendor’s own URL:
var trustedAssets = new Dictionary<string, byte[]>
{
[assetUrl] = File.ReadAllBytes(assetPath)
};
var audit = new ResourceAudit(trustedAssets);
var loadOptions = new WebLoadOptions { ExternalResourceHandler = audit };
using (var converter = new Converter(
() => File.OpenRead(uploadPath), (LoadContext _) => loadOptions))
{
converter.Convert(outputPath, new PdfConvertOptions());
}
return audit.Loaded;
ResourceAudit’s handler checks the requested URL against a dictionary of
trusted assets; when it matches, it sets ExternalResourceLoadingArgs.Data
to local bytes and Skipped to false, so the logo appears in the output
PDF while the invoice’s other five references stay blocked.
Step 4 - Reject a Misconfigured Whitelist Entry Early
A whitelist entry that is only a host name, not an absolute URI prefix, fails the conversion instead of silently matching too much:
var loadOptions = new WebLoadOptions
{
WhitelistedResources = new List<string> { entry }
};
try
{
using var converter = new Converter(
() => File.OpenRead(uploadPath), (LoadContext _) => loadOptions);
converter.Convert(outputPath, new PdfConvertOptions());
return null;
}
catch (InvalidLoadOptionsException exception)
{
return exception.Message;
}
Passing cdn.example.com instead of https://cdn.example.com/brand/
throws InvalidLoadOptionsException with the message: Whitelisted resource 'cdn.example.com' is not valid. Expected an absolute URI prefix without a query, fragment or encoded '/', '\' or '.', such as 'https://cdn.example.com/assets/' or 'file:///C:/assets/'. Catching this at
conversion time, in a configuration review or a startup check, is far
cheaper than discovering it from a wrongly-matched resource in production.
Catch it before it ships.
Why would a URL whitelist entry ever get rejected?
WhitelistedResources only accepts absolute URI prefixes, such as
https://cdn.example.com/brand/ or file:///C:/assets/. Earlier versions
matched an entry wherever it appeared in the URL, so cdn.example.com
also matched an attacker’s address that merely contained those words.
Since 26.10 that entry fails the load with InvalidLoadOptionsException,
and the sample’s last method prints the exact message.
Vendor SVG Attachments Carry the Same Risk
A facilities drawing attached to a construction invoice is still just an
SVG file, and it goes through SvgLoadOptions instead of WebLoadOptions.
Same contract, different element: the ExternalResourceHandler applies to
its <image> references the way it applied to the invoice’s <img> tags,
and any inline <script> the drawing carries does not run:
var audit = new ResourceAudit();
var loadOptions = new SvgLoadOptions { ExternalResourceHandler = audit };
using (var converter = new Converter(
() => File.OpenRead(uploadPath), (LoadContext _) => loadOptions))
{
converter.Convert(outputPath, new PdfConvertOptions());
}
return audit.Loaded;
This is exactly the construction-invoice scenario from the opening: a drawing that looks like nothing more than a floor plan can still carry a reference to your cloud metadata endpoint, a local file, or an attacker’s own server. In the working sample all three are blocked, and the caption renders exactly as drawn, because the script attached to it never executes.
Behavior Before and After
| Check | Converting straight from the saved path | This approach |
|---|---|---|
| Same-folder sibling file | Loads automatically | Blocked - stream input removes the base folder |
| Vendor logo | Blocked; the PDF shows only its alt text | Supplied from a reviewed local file |
| Cloud metadata / internal address | Blocked by the default | Blocked by the default |
| Host-name-only whitelist entry | Rejected with InvalidLoadOptionsException |
Rejected the same way, caught by a startup check |
| Inline script in the upload | Does not run | Does not run |
Two rows change: the logo and the sibling file. The sibling file is the one a network-level review would never catch, because no network request is involved.
Other Vendor-Document Scenarios Where This Applies
The same ExternalResourceHandler property exists on the load options for
several other formats vendor portals commonly accept, so the pattern above
is not limited to HTML and SVG:
- Vendor-submitted Word or Excel attachments:
WordProcessingLoadOptionsandSpreadsheetLoadOptionsexpose the same handler contract. - E-mailed invoices:
EmailLoadOptionsexposes the same handler for the remote images an e-mail body references. - XBRL financial filings:
FinanceLoadOptionscovers taxonomy references in vendor-submitted XBRL documents.
Getting Started With Your Vendor Upload Pipeline
You’ve seen how GroupDocs.Conversion handles untrusted vendor uploads safely. Here’s what to do next:
- Start free: get a temporary license to remove the evaluation limits
- Read the full resource policy: skip loading of external resources covers
WhitelistedResources,IExternalResourceHandler, and script handling in detail - Check what touches the network at all: network access and data privacy
- Follow the step-by-step guide: convert untrusted uploads safely - use case
- Browse the working sample: secure-untrusted-document-upload-conversion-dotnet on GitHub