diff --git a/docs/phoenixmldb/deployment/embedded-mode.md b/docs/phoenixmldb/deployment/embedded-mode.md index bdb7eaf..95ab0f1 100644 --- a/docs/phoenixmldb/deployment/embedded-mode.md +++ b/docs/phoenixmldb/deployment/embedded-mode.md @@ -68,6 +68,13 @@ var options = new DatabaseOptions using var db = new XmlDatabase("./data", options); ``` +### Resource access + +`DocumentDatabase.ResourceAccessPolicy` defaults to `ResourceAccessPolicy.DenyAll`: queries and +stylesheets read stored documents only, with no local files or network requests. Allow specific +directories and HTTP origins with `ResourceAccessPolicy.Create(...)`. See +[Resource Access](../resource-access.md). + ## Lifecycle Management ### Application Startup diff --git a/docs/phoenixmldb/deployment/server-mode.md b/docs/phoenixmldb/deployment/server-mode.md index af71e57..3d38f84 100644 --- a/docs/phoenixmldb/deployment/server-mode.md +++ b/docs/phoenixmldb/deployment/server-mode.md @@ -259,6 +259,13 @@ from the token's `permission` claim. | `RequireWrite` | `write`, `admin`, `full` | | `RequireAdmin` | `admin`, `full` | +## Resource Access + +Queries and stylesheets sent to either server can read only the stored documents, unless you list +directories in `PhoenixmlDb:ResourceAccess:AllowedFileRoots` or origins in +`PhoenixmlDb:ResourceAccess:AllowedHttpOrigins`. Both are empty by default and validated at +startup. See [Resource Access](../resource-access.md). + ## TLS Configuration ### Generate Certificates diff --git a/docs/phoenixmldb/resource-access.md b/docs/phoenixmldb/resource-access.md new file mode 100644 index 0000000..b9e7976 --- /dev/null +++ b/docs/phoenixmldb/resource-access.md @@ -0,0 +1,129 @@ +--- +title: Resource Access +description: Queries and stylesheets can read only stored documents unless the operator allows local directories or HTTP origins +sort: 8 +--- + +# Resource Access + +> **Breaking change** (phoenixml `main`, a2b9963, issue #59): queries and stylesheets supplied by +> callers are **deny-by-default**. They can read the documents stored in the database, and nothing +> else, until the operator allows specific directories or HTTP origins. This applies to the +> embedded engine, the gRPC server (including its REST query endpoint) and the REST server. + +PhoenixmlDb can't know what data goes into a database, or what an application built on it is +meant to expose. So nothing outside the stored documents is reachable by default, and each +inclusion is a deliberate, explicit choice by whoever runs the system. In the owner's words: + +> We have no idea what kind of data is going into these databases, and we're going to have to rely +> on customers having the foreknowledge that they're building a system to intentionally +> include/remove data for specific purposes. + +## What is blocked by default + +A caller's XQuery or XSLT cannot read local files or make network requests through: + +- `fn:doc()`, `document()`, `fn:collection()` with a URI that isn't a stored document or collection +- `fn:unparsed-text()`, `fn:unparsed-text-lines()`, `fn:unparsed-text-available()`, `fn:json-doc()` +- `xsl:source-document`, `xsl:import`, `xsl:include` +- `import module … at` and `import schema … at` location hints +- `fn:transform` stylesheet locations, and `xsl:evaluate` +- external DTDs and external entities + +**Stored documents resolve as before.** `doc()` and `collection()` of stored documents work under +every policy. + +In XQuery, `fn:doc()`, `fn:doc-available()` and `fn:collection()` **only ever** resolve stored +documents. An allowlist doesn't make them read files; use `fn:unparsed-text()` or `fn:json-doc()` +for an allowed file. `fn:transform` called from XQuery stays denied under any allowlist and runs +only under the `Unrestricted` policy. + +## Allowing directories and origins (servers) + +Both servers read the same settings, empty by default: + +| Setting | Value | +|---|---| +| `PhoenixmlDb:ResourceAccess:AllowedFileRoots` | Absolute paths of directories that exist. Files under them may be read. | +| `PhoenixmlDb:ResourceAccess:AllowedHttpOrigins` | Origins, `scheme://host[:port]`, with no path, query, fragment or user info. `http` or `https` only. | + +As environment variables, index each entry: + +```bash +export PhoenixmlDb__ResourceAccess__AllowedFileRoots__0=/srv/phoenixml/shared +export PhoenixmlDb__ResourceAccess__AllowedHttpOrigins__0=https://schemas.example.com +``` + +```json +{ + "PhoenixmlDb": { + "ResourceAccess": { + "AllowedFileRoots": [ "/srv/phoenixml/shared" ], + "AllowedHttpOrigins": [ "https://schemas.example.com" ] + } + } +} +``` + +- **Paths are canonicalised.** A `..` that climbs out of a root, or a symbolic link that points + outside it, is denied. +- **The port is part of the origin.** `https://example.com` doesn't allow `https://example.com:8443`. +- **HTTP fetches don't follow redirects.** A redirect is a failed read, never a request to the + redirect target. +- **Invalid values stop the server from starting**, with a message naming the setting (for example + `PhoenixmlDb:ResourceAccess:AllowedFileRoots:0`). +- **The effective allowlist is logged at startup.** +- **The allowlist is configuration only.** It can't be changed at runtime or through an API. + +## Embedded applications + +`DocumentDatabase.ResourceAccessPolicy` (namespace `PhoenixmlDb.Storage.Security`) defaults to +`ResourceAccessPolicy.DenyAll`. To opt in: + +```csharp +using PhoenixmlDb.Storage.Security; + +var options = new ResourceAccessOptions(); +options.AllowedFileRoots.Add("/srv/phoenixml/shared"); +options.AllowedHttpOrigins.Add("https://schemas.example.com"); + +db.ResourceAccessPolicy = ResourceAccessPolicy.Create(options); +``` + +`Create` throws `ArgumentException` listing every invalid setting; `options.Validate()` returns the same messages without throwing. Empty options give `DenyAll`. + +`ResourceAccessPolicy.Unrestricted` restores the behaviour before this change: queries and +stylesheets can read any file or URL the process can. **Use it only when every query and +stylesheet is your own trusted code**, never for text that comes from users. + +## Errors + +A denied access raises an error in the query or transformation: + +| Access | Error | +|---|---| +| `import module` / `import schema` location | `XQST0059` | +| `fn:unparsed-text()` and related functions | `FOUT1170` | +| `fn:doc()` and related functions | `FODC…` codes | + +The REST server answers a denied query or transformation with `400`. The response doesn't include +the denied content or a stack trace. + +## Currently refused by the REST server + +The REST server refuses stylesheets that use the following with `400`, **even when an allowlist is +configured**: + +- `xsl:evaluate` +- calls or function references to `fn:transform`, `fn:json-doc`, `fn:load-xquery-module` and + `fn:function-lookup` +- shadow attributes (`_name="…"`) on XSL elements +- `http:` and `https:` `xsl:import-schema` locations; use a schema file in an allowed directory + +Allowed HTTP documents and imported stylesheets are fetched by the server itself, without +following redirects. + +## Next Steps + +- [Server Mode](deployment/server-mode.md): authentication and the other server settings +- [Embedded Mode](deployment/embedded-mode.md) diff --git a/docs/release-notes.md b/docs/release-notes.md index 5288eef..8b9f578 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -24,6 +24,17 @@ setting and startup check. **The gRPC server has no authentication yet.** Don't expose it outside a trusted network. +### Breaking: queries and stylesheets can't read files or URLs by default + +Since phoenixml `main` a2b9963 (issue #59), XQuery and XSLT supplied by callers can read only the +stored documents, in the embedded engine and in both servers. Local files, HTTP requests, module +and stylesheet imports by location, `xsl:evaluate`, and external DTDs and entities are denied +until the operator allows specific directories or origins +(`PhoenixmlDb:ResourceAccess:AllowedFileRoots` / `AllowedHttpOrigins` on the servers, +`DocumentDatabase.ResourceAccessPolicy` embedded). `doc()` and `collection()` of stored documents +work as before. An embedded application that relied on the old behaviour for trusted code can set +`ResourceAccessPolicy.Unrestricted`. See [Resource Access](phoenixmldb/resource-access.md). + ### Breaking for monitoring: health endpoints are status-only Since phoenixml `main` 170adf3 (issue #51), `/health`, `/health/live` and `/health/ready` return