API Reference
Resource Policy
#Resource Policy
Control what external resources XSLT and XQuery code can access. Essential for running transformations in server environments where untrusted stylesheets or queries must be sandboxed.
Upgrade to 2.5.1. Before PhoenixmlDb.XQuery 2.5.0 and PhoenixmlDb.Xslt 2.5.1, a policy was enforced only when loading documents, so
unparsed-text,json-doc, imports,xsl:source-document,fn:transformand HTTP redirects could reach what it forbade (GHSA-wjxc-7p24-xf7w, GHSA-86rg-wxgp-9p5j). This page describes 2.5.1.
#Quick Start
// Lock down for server use — no filesystem, no network
var transformer = new XsltTransformer();
transformer.ResourcePolicy = ResourcePolicy.ServerDefault;
await transformer.LoadStylesheetAsync(stylesheet);
var result = await transformer.TransformAsync(inputXml);
// doc('file:///etc/passwd') → ResourceAccessDeniedException
#Choosing a policy
ResourcePolicy is optional on XsltTransformer, XsltTransformOptions, XQueryFacade and
QueryEngine. Leaving it unset is a deliberate choice, not an oversight:
|
Who runs the code |
Use |
|---|---|
|
You wrote it: command-line tools, build pipelines, doc generators, test suites |
no policy ( |
|
Someone else wrote it: a server running users' queries or stylesheets |
|
|
Everything is served by your own |
|
No policy means no restrictions: queries and stylesheets can read any file or URL the process
can, and behave exactly as before 2.5. ResourcePolicy.Unrestricted is not the same as no
policy:
-
DTDs:
Unrestricteddoesn't resolve external DTD subsets or external entities, infn:parse-xmlor in a stylesheet's DOCTYPE. The internal subset still works. With no policy, they resolve. -
HTTP: under any policy,
Unrestrictedincluded, a redirect is re-checked against the policy at every hop, and remote XQuery modules are fetched fresh on each compilation instead of from the process-wide cache. With no policy, both behave as before.
#Presets
#ResourcePolicy.ServerDefault
Denies all external access by default: no file system, no network, no DTDs, no xsl:evaluate. Only documents pre-loaded by the application or served by a custom resolver are accessible. Limits: 100 document loads, 10 result documents, 10 MB output, 50 unparsed-text loads.
#ResourcePolicy.InMemoryOnly
No external access at all. Only in-memory documents provided via a custom IResourceResolver.
#ResourcePolicy.Unrestricted
All schemes allowed for reading, importing and writing, and xsl:evaluate on. External DTDs and entities are off, and redirects are re-checked, so it is not the same as no policy (see Choosing a policy).
#Builder API
// Allow HTTPS reads from a specific domain
transformer.ResourcePolicy = ResourcePolicy.CreateBuilder()
.AllowReadFrom("https", host: "api.example.com")
.AllowReadFrom("https", host: "cdn.example.com", pathPrefix: "/schemas/")
.WithMaxDocumentLoads(100)
.Build();
// Separate read and write policies
transformer.ResourcePolicy = ResourcePolicy.CreateBuilder()
.AllowReadFrom("https")
.AllowWriteTo("s3")
.WithMaxResultDocuments(10)
.Build();
// Allow imports from specific paths
transformer.ResourcePolicy = ResourcePolicy.CreateBuilder()
.AllowImportFrom("file", pathPrefix: "/app/stylesheets/")
.AllowReadFrom("https")
.Build();
// A non-default port must be named (UriRule.AnyPort allows any)
transformer.ResourcePolicy = ResourcePolicy.CreateBuilder()
.AllowReadFrom("https", "api.example.com", "/v1/", port: 8443)
.Build();
// DTD processing and xsl:evaluate are off in a built policy until enabled
transformer.ResourcePolicy = ResourcePolicy.CreateBuilder()
.AllowReadFrom("file", pathPrefix: "/data/")
.AllowDtdProcessing()
.AllowXslEvaluate()
.Build();
#How rules match
-
Access kinds are separate. A read rule admits reads only; importing a module or stylesheet needs an import rule (
AllowImportFrom), and writing a write rule. A scheme allowed withAllowScheme(or"*") admits every kind. -
An empty rule list denies.
-
Ports: a host rule admits only the scheme's default port unless it names one.
-
Paths match whole segments (
/app/datadoesn't admit/app/database), case-sensitively where the file system is, against the canonical path with symbolic links resolved. -
A rooted path such as
/etc/xis afile:URI, not a relative reference. -
ResourcePolicy.Authorize(uri, access)(orTryAuthorize) applies the same check from your own code and returns the URI to open.
#Upgrading rules built before 2.5
Two mistakes deny access that a pre-2.5 rule allowed. Both fail closed:
-
Name the port for an origin on a non-default port. A rule for
https://api.example.com:8443built without a port admits only port 443:csharp var origin = new Uri("https://api.example.com:8443"); builder.AllowReadFrom(origin.Scheme, origin.Host, pathPrefix: null, port: origin.Port);Pass
UriRule.AnyPortonly if any port really is acceptable. -
Build file prefixes from a local path, not from
Uri.AbsolutePath. File rules are compared with the canonical local path.AbsolutePathis percent-escaped, so a root containing a space or a non-ASCII character (/srv/my%20data/) never matches. UseUri.LocalPathorPath.GetFullPath(...):csharp builder.AllowReadFrom("file", pathPrefix: Path.GetFullPath("/srv/my data/"));
#Custom Resource Resolver
The IResourceResolver interface lets you plug in any storage backend. XSLT/XQuery code uses standard functions (doc(), unparsed-text(), collection()) and your resolver handles the URI.
public class S3ResourceResolver : ResourceResolverBase
{
private readonly IAmazonS3 _s3;
private readonly string _bucket;
public S3ResourceResolver(IAmazonS3 s3, string bucket)
{
_s3 = s3;
_bucket = bucket;
}
public override XdmDocument? ResolveDocument(string uri, ResourceAccessKind access)
{
if (!uri.StartsWith("s3://")) return null;
var key = uri.Replace("s3://", "").TrimStart('/');
var response = _s3.GetObjectAsync(_bucket, key).Result;
using var reader = new StreamReader(response.ResponseStream);
var xml = reader.ReadToEnd();
var store = new XdmDocumentStore();
return store.LoadFromString(xml, uri);
}
}
// Wire it up
transformer.ResourcePolicy = ResourcePolicy.CreateBuilder()
.WithResourceResolver(new S3ResourceResolver(s3Client, "my-bucket"))
.AllowReadFrom("s3")
.Build();
Now XSLT code can do:
<xsl:variable name="config" select="doc('s3://my-bucket/config.xml')"/>
#IResourceResolver Methods
|
Method |
Purpose |
|---|---|
|
|
Load XML documents ( |
|
|
Load text files ( |
|
|
Load document collections ( |
|
|
Write output ( |
|
|
Load stylesheets ( |
|
|
Check availability ( |
|
|
Check availability ( |
Use ResourceResolverBase as a base class — it returns null for all methods, so you only override what you need.
#What Gets Controlled
|
Access point |
Checked as |
|---|---|
|
|
read |
|
|
read ( |
|
|
import for the stylesheet, read for the source; the nested transformation runs under the caller's policy |
|
|
import ( |
|
|
import |
|
External DTDs and entities in |
only with |
|
|
only with |
|
|
write |
|
HTTP redirects |
every hop re-checked |
Checks run while a stylesheet loads as well as while it runs: the stylesheet pre-fetch and static
expressions (use-when, static parameters, shadow attributes) are evaluated under the policy.
doc-available, unparsed-text-available and stream-available return false for a refused
resource, and a refused import reads as "not found", so neither reveals whether a file exists.
#Resource Budgets
|
Property |
Default (Unrestricted) |
Default (ServerDefault) |
Purpose |
|---|---|---|---|
|
|
0 (unlimited) |
100 |
Limit |
|
|
1000 |
10 |
Limit |
|
|
50 MB |
10 MB |
Limit primary output size |
|
|
0 (unlimited) |
50 |
Limit |
#XQuery
The same ResourcePolicy works on XQueryFacade:
var xquery = new XQueryFacade();
xquery.ResourcePolicy = ResourcePolicy.ServerDefault;
var result = await xquery.EvaluateAsync("doc('file:///etc/passwd')");
// → ResourceAccessDeniedException
#Comparison with Saxon
|
Feature |
Saxon |
PhoenixmlDb |
|---|---|---|
|
Protocol filtering |
|
|
|
Host/path scoping |
No |
Yes — per-host, per-path rules |
|
Separate read/write |
No |
Yes — |
|
Custom resolver |
|
|
|
Default |
Allow all |
No policy allows all; |
|
Resource budgets |
No |
Max document loads, result documents, output size, text loads |
|
Import filtering |
No |
Yes — separate |