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();
// fn:transform is on unless turned off (2.9.0)
transformer.ResourcePolicy = ResourcePolicy.CreateBuilder()
.AllowReadFrom("file", pathPrefix: "/data/")
.AllowTransformFunction(false)
.Build();
AllowTransformFunction(bool allow = true)
(2.9.0, read back as
ResourcePolicy.AllowTransformFunction). fn:transform runs a stylesheet the caller supplies.
It is allowed unless a host turns it off, in every preset and with no policy at all, so nothing
changes for a host that does not call this. Turned off, every way to call the function (by name,
through a function item, function-lookup, partial application, fn:apply, and in XSLT
xsl:evaluate and static expressions) fails with FOXT0001 before the stylesheet is read, and
function-lookup and function-available do not report the function, at run time or in
use-when. A function the host can turn off this way implements IHostGatedFunction.
#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.
#Supplying the content yourself (2.7.0)
Normally the engine checks a location against the policy and then opens it by name. Those are two steps, so a file replaced between them, for example swapped for a link out of the allowed folder, is read. Since 2.7.0 a resolver can close that gap by handing the engine the content itself:
|
Member |
Purpose |
|---|---|
|
|
Return a |
|
|
|
Relative references inside supplied content resolve against its base URI and come back to the
resolver. Supplied content is the host's own decision and is not checked against the policy's URI
rules. Hosts that run untrusted queries or stylesheets should supply content this way and set
SuppliesAllContent.
#Which module asks (2.9.0)
ResourceRequest.ModuleUri (Uri?) is where the module that makes a request was loaded from: the
location the host gave for the query or stylesheet, the URI an import named, or the base URI the
host gave with content it supplied. The module cannot change it; xml:base and declare base-uri
do not. It is null when no module makes the request (a document another document includes, a
redirect) or the engine knows no location for the module.
Since 2.9.0 it is set for a document, a text or a module that a query asks for, and for the requests
the XSLT compiler makes: xsl:import, xsl:include, the schema documents of xsl:import-schema,
and the parameter-document of xsl:output. An XPath expression in a stylesheet carries its
module's location (XQueryExpression.EmbeddedInModule, set by the XSLT compiler), so its loads
report it too. XsdSchemaProvider passes the importing module with each schema document it asks
for.
#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. Off with |
|
|
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 |
#String Length
MaxOutputSize limits only the serialized result. An expression can build a very large string
from no input, faster than a time limit can cancel it, so a host that runs untrusted code sets a
string-length limit as well as a time limit. The limit is not part of ResourcePolicy:
|
Engine |
Setting |
Default |
|---|---|---|
|
XQuery |
|
|
|
XSLT |
|
|
The value is the longest string, in UTF-16 code units, that one operation may build. An operation
that goes past it raises FOER0000, which a query or stylesheet can catch with try/catch.
-
It applies to
concat,string-join,||, string constructors,codepoints-to-string,replace,serialize,xml-to-json, the case, URI andnormalize-unicodefunctions, and the text that node constructors join. -
In XSLT it also applies to
xsl:value-of, the content ofxsl:attribute,xsl:comment,xsl:processing-instructionandxsl:namespace, and attribute and text value templates.fn:transformpasses the limit to the transformation it starts. -
string-join,replace, string constructors and node constructors stop before the string passes the limit, so they do not allocate the full string. -
The limit is per string, not per query.
QueryExecutionContext.CheckStringLength(long)applies it in host code and extension functions.
var engine = new QueryEngine();
var context = engine.CreateContext(limits: new QueryExecutionLimits { MaxStringLength = 1_000_000 });
var transformer = new XsltTransformer { MaxStringLength = 1_000_000 };
#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 |