Skip to content

Commit 7afc4ec

Browse files
authored
feat(go-server): add function allowlist (#1150)
1 parent f693695 commit 7afc4ec

48 files changed

Lines changed: 3332 additions & 45 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ This repository contains a set of related packages, spanning the core Mosaic arc
5353

5454
* [`duckdb-server`](https://github.com/uwdata/mosaic/tree/main/packages/server/duckdb-server): A Python-based server that runs a local DuckDB instance and support queries over Web Sockets or HTTP, returning data in either [Apache Arrow](https://arrow.apache.org/) or JSON format.
5555
* [`duckdb-server-rust`](https://github.com/uwdata/mosaic/tree/main/packages/server/duckdb-server-rust): A Rust-based server similar to `duckdb-server` (Python) and `mosaic-duckdb` (Node.js) with additional support for HTTP/2. We are still evaluating what server component works best. DuckDB support for Rust is often delayed compared to Python.
56-
* [`duckdb-server-go`](https://github.com/uwdata/mosaic/tree/main/packages/server/duckdb-server-go): A Go-based server similar to `duckdb-server` (Python) and `mosaic-duckdb` (Node.js) with additional support for HTTP/2. It has experimental support for multi-tenant access control, function blocklisting, and other features to harden the server for production use.
56+
* [`duckdb-server-go`](https://github.com/uwdata/mosaic/tree/main/packages/server/duckdb-server-go): A Go-based server similar to `duckdb-server` (Python) and `mosaic-duckdb` (Node.js) with additional support for HTTP/2. It has experimental support for multi-tenant access control, function allowlisting and blocklisting, and other features to harden the server for production use.
5757
* [`mosaic-duckdb`](https://github.com/uwdata/mosaic/tree/main/packages/server/duckdb): A Promise-based Node.js API to DuckDB, along with a data server that supports transfer of [Apache Arrow](https://arrow.apache.org/) and JSON data over either Web Sockets or HTTP. Due to quality and maintenance issues involving the Node.js DuckDB client and Arrow extension, we recommend using the Python-based `duckdb-server` package instead. However, we retain this package for both backwards compatibility and internal testing use.
5858

5959
### Examples (`packages/examples`)

packages/server/duckdb-server-go/README.md

Lines changed: 85 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ You can customize the server behavior with the following command-line flags:
3333
- `--key <path>`: Path to a TLS private key file to enable HTTPS.
3434
- `--schema-match-headers`: Comma-separated list of headers to match against schema names for multi-tenant access control (e.g., `X-Tenant-Id,verified-user-id`).
3535
- `--load-extensions`: Comma-separated list of extensions to install and load at startup. Use a pipe after the extension name to specify a DuckDB repository alias. Unspecified repositories use DuckDB's default (e.g. `mysql_scanner,netquack|community,aws|core_nightly`).
36-
- `--function-blocklist`: Comma-separated list of functions to block, useful for blocking functions that may pose security or performance risks. (e.g., 'bigquery_query,read_parquet')`
36+
- `--function-blocklist`: Comma-separated list of exact function names to block, useful for blocking functions that may pose security or performance risks (e.g. `bigquery_query,read_parquet`).
37+
- `--function-allowlist`: Comma-separated list of exact function names to add to the reviewed defaults. Names are matched case-insensitively, repeated flags accumulate names, and an explicitly empty value enables only the defaults.
3738

3839
By default, the server will look for `localhost.pem` and `localhost-key.pem` in the current directory to enable HTTPS if the `--cert` and `--key` flags are not provided.
3940

@@ -79,6 +80,80 @@ errors are logged and returned as sanitized 500 responses. Authorization can all
7980
and exact SQL, but cannot rewrite SQL or sandbox the shared process, filesystem, network, extensions, catalogs, or
8081
credentials.
8182

83+
### Function Policies
84+
85+
Use an allowlist when the server should accept only reviewed functions and operators. An explicitly empty value enables
86+
the defaults without adding application-specific names:
87+
88+
```sh
89+
duckdb-server-go --function-allowlist=
90+
```
91+
92+
Without `--function-allowlist`, the server remains unrestricted. The binary intentionally exposes only policy
93+
activation and exact additions; use a custom binary embedding `pkg/query` for exclusions, exact-only policies, or
94+
extension groups.
95+
96+
Programs embedding `pkg/query` can apply the same policy and add application functions with:
97+
98+
```go
99+
query.WithFunctionAllowlist(query.FunctionAllowlistOptions{
100+
Include: append(functionset.Spatial.Elevated(), "my_function"),
101+
})
102+
```
103+
104+
By default, configured policies use `functionset.DefaultFunctions()`, which contains reviewed built-ins and every
105+
[core extension](https://duckdb.org/docs/current/core_extensions/overview)'s `Compute()` group. `Elevated()` requires
106+
explicit admission, and `All()` returns both groups. These Go helpers return fresh slices; the CLI accepts exact names only.
107+
108+
The table records unique names reviewed against DuckDB 1.5.5. A name is elevated if any overload has elevated behavior.
109+
An empty row means the extension has no reviewed function-call names, not that it has no other capabilities.
110+
111+
| Extension | Compute | Elevated | Classification and status |
112+
| --- | ---: | ---: | --- |
113+
| `Autocomplete` | 1 | 3 | Parser check; completion and parser controls are elevated. |
114+
| `Avro` | 0 | 1 | Reader only. |
115+
| `AWS` | 0 | 1 | Credential and provider operation. |
116+
| `Azure` | 0 | 0 | Filesystem integration with no reviewed function-call names. |
117+
| `Delta` | 2 | 9 | Local parser/test helpers; scans, metadata I/O, and writes are elevated. |
118+
| `DuckLake` | 1 | 21 | Local hash helper; catalog, scan, metadata, and mutation operations are elevated. |
119+
| `Encodings` | 0 | 0 | CSV codec integration with no reviewed function-call names. |
120+
| `Excel` | 2 | 1 | Value conversion; the sheet reader is elevated. |
121+
| `FTS` | 1 | 2 | Text stemming; index creation and mutation are elevated. |
122+
| `HTTPFS` | 0 | 0 | Filesystem integration with no reviewed function-call names. |
123+
| `Iceberg` | 2 | 14 | Value helpers; scans, catalogs, metadata I/O, and writes are elevated. |
124+
| `ICU` | 179 | 7 | Deterministic collation and calendar computation; current-time names are elevated. |
125+
| `Inet` | 11 | 0 | IP value operations only. |
126+
| `JSON` | 33 | 9 | Value parsing and serialization; readers, SQL execution, and plan inspection are elevated. |
127+
| `Lance` | 0 | 12 | Source-pinned scans and metadata operations. |
128+
| `MotherDuck` | 0 | 198 | Best-effort observed proprietary runtime snapshot; all names are elevated. |
129+
| `MySQL` | 0 | 5 | Connector and scanner operations. |
130+
| `ODBC` | 0 | 11 | Connector and scanner operations. |
131+
| `Parquet` | 2 | 9 | `VARIANT` conversion; file, metadata, bloom, and key operations are elevated. |
132+
| `Postgres` | 2 | 8 | Value helpers; connector and scanner operations are elevated. |
133+
| `Quack` | 3 | 9 | Protocol value helpers; remote and session operations are elevated. |
134+
| `Spatial` | 151 | 13 | Geometry computation; readers, index/catalog access, random generation, and resource-capable transforms are elevated. |
135+
| `SQLite` | 0 | 3 | Connector and scanner operations. |
136+
| `TPCDS` | 2 | 2 | Query and answer text; data generators are elevated. |
137+
| `TPCH` | 2 | 2 | Query and answer text; data generators are elevated. |
138+
| `UI` | 0 | 5 | HTTP server lifecycle, URL, and status operations. |
139+
| `UnityCatalog` | 0 | 4 | Attached-catalog and checkpoint operations; the generated registry is incomplete. |
140+
| `Vortex` | 0 | 2 | Readers verified against the pinned nested source revision. |
141+
| `VSS` | 0 | 5 | Index access and management operations. |
142+
143+
These groups authorize names only; extension loading and file or network access are separate concerns. Validation is
144+
syntactic and name-only: it does not bind function identity, inspect arguments, expand macros or views, recursively inspect
145+
SQL strings, or cover replacement scans and attached-table binding. Keep catalogs and the search path trusted, and enforce
146+
resource access outside this policy. Pre-provisioned views and attached tables can deliberately expose curated datasets
147+
while reader functions remain excluded; catalog integrity and process resource controls then carry the boundary.
148+
149+
In Go, `Exclude` wins over `Include`, and `DisableDefaults` creates an exact-only policy. Omitting
150+
`WithFunctionAllowlist` is unrestricted; configuring an exact-empty policy denies all function calls. A function
151+
allowlist cannot be combined with a non-empty blocklist, and any configured function policy rejects `exec` requests.
152+
153+
Spatial compute defaults cover Mosaic rendering over existing geometry data, but the `ST_Read` loader remains elevated.
154+
Current-time functions are omitted from defaults because persistent cache entries do not expire by default; keyword forms
155+
such as `CURRENT_DATE` are not function nodes and remain outside this policy.
156+
82157
### Multi-Tenant Access Control
83158

84159
`schema-match-headers` isn't part of the mosaic server API, but is provided here as an example of how to have
@@ -108,19 +183,19 @@ multiple users / customers share the same DuckDB server instance while restricti
108183

109184
_Note:_ Schema matching authorizes schema references in submitted SQL; it does not isolate the shared DuckDB process,
110185
filesystem, network, extensions, or credentials. It assumes a single catalog; attached catalogs are outside this policy
111-
boundary, and explicitly catalog-qualified table, `SHOW`, and function references are rejected. The function blocklist
112-
applies only to explicit function calls. Schema matching does not restrict catalog metadata returned by functions such as
113-
`duckdb_tables()` and `pragma_table_info()`. If metadata is sensitive, add the exact metadata-function names exposed by the
114-
deployment to `--function-blocklist`; wildcard patterns such as `duckdb_*` are not supported, and the list must be reviewed
186+
boundary, and explicitly catalog-qualified table, `SHOW`, and function references are rejected. Function allowlists and
187+
blocklists apply only to explicit function calls. Schema matching does not restrict catalog metadata returned by functions
188+
such as `duckdb_tables()` and `pragma_table_info()`. If metadata is sensitive, allow or block the exact metadata-function
189+
names exposed by the deployment; wildcard patterns such as `duckdb_*` are not supported, and the policy must be reviewed
115190
when DuckDB or its extensions change. To restrict file-reading functions, also enable schema matching so DuckDB replacement
116191
scans such as `FROM 'data.parquet'` are rejected as unqualified table references. These controls are not a sandbox: run the
117192
server with access only to external resources that are safe for every tenant.
118193

119-
If either `--schema-match-headers` or `--function-blocklist` is configured, `json` and `arrow` requests are limited to
120-
statements DuckDB can serialize for validation; unsupported forms such as `PRAGMA` and `SET` are rejected, with HTTP
121-
requests receiving a 400 response. All `exec` requests are also rejected until full-statement authorization is supported.
122-
This includes every `Coordinator.exec(...)` call, such as data loading, preloading, and DDL/DML. Mosaic pre-aggregation
123-
also uses `exec` to create schemas and tables, so set `preagg: { enabled: false }` in this mode.
194+
If `--schema-match-headers`, `--function-blocklist`, or `--function-allowlist` is configured, `json` and `arrow` requests
195+
are limited to statements DuckDB can serialize for validation; unsupported forms such as `PRAGMA` and `SET` are rejected,
196+
with HTTP requests receiving a 400 response. All `exec` requests are also rejected until full-statement authorization is
197+
supported. This includes every `Coordinator.exec(...)` call, such as data loading, preloading, and DDL/DML. Mosaic
198+
pre-aggregation also uses `exec` to create schemas and tables, so set `preagg: { enabled: false }` in this mode.
124199

125200
## API
126201

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package main
2+
3+
import "strings"
4+
5+
type optionalCommaListFlag struct {
6+
values []string
7+
set bool
8+
}
9+
10+
func (f *optionalCommaListFlag) Set(value string) error {
11+
f.set = true
12+
if value != "" {
13+
f.values = append(f.values, strings.Split(value, ",")...)
14+
}
15+
return nil
16+
}
17+
18+
func (f *optionalCommaListFlag) String() string {
19+
return strings.Join(f.values, ",")
20+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package main
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestOptionalCommaListFlag(t *testing.T) {
11+
var flag optionalCommaListFlag
12+
13+
assert.False(t, flag.set)
14+
assert.Empty(t, flag.values)
15+
16+
require.NoError(t, flag.Set("md5,range"))
17+
require.NoError(t, flag.Set("+"))
18+
assert.True(t, flag.set)
19+
assert.Equal(t, []string{"md5", "range", "+"}, flag.values)
20+
assert.Equal(t, "md5,range,+", flag.String())
21+
}
22+
23+
func TestOptionalCommaListFlagPreservesExplicitEmpty(t *testing.T) {
24+
var flag optionalCommaListFlag
25+
26+
require.NoError(t, flag.Set(""))
27+
assert.True(t, flag.set)
28+
assert.Empty(t, flag.values)
29+
assert.Empty(t, flag.String())
30+
}

packages/server/duckdb-server-go/main.go

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ import (
1919
)
2020

2121
func main() {
22+
os.Exit(run())
23+
}
24+
25+
func run() int {
2226
dbPath := flag.String("database", ":memory:", "Path of database file (e.g., \"database.db\". \":memory:\" for in-memory database)")
2327
address := flag.String("address", "localhost", "HTTP Address")
2428
port := flag.String("port", "3000", "HTTP Port")
@@ -31,6 +35,8 @@ func main() {
3135
schemaMatchHeadersStr := flag.String("schema-match-headers", "", "Comma-separated list of headers to match against schema names for multi-tenant access control (e.g., \"X-Tenant-Id,verified-user-id\")")
3236
extensionsStr := flag.String("load-extensions", "", "Comma-separated list of extensions to install and load at startup. Use a pipe after the extension name to specify a DuckDB repository alias. Unspecified repositories use DuckDB's default (e.g. mysql_scanner,netquack|community,aws|core_nightly).")
3337
functionBlocklistStr := flag.String("function-blocklist", "", "Comma-separated list of functions to block, useful for blocking functions that may pose security or performance risks. (e.g., 'bigquery_query,read_parquet')")
38+
var functionAllowlist optionalCommaListFlag
39+
flag.Var(&functionAllowlist, "function-allowlist", "Comma-separated exact names to add to the reviewed default allowlist. An empty value enables only the defaults; names are matched case-insensitively.")
3440
flag.Parse()
3541

3642
var schemaMatchHeaders []string
@@ -52,7 +58,7 @@ func main() {
5258

5359
if err := extensions.Validate(*extensionsStr); err != nil {
5460
logger.Error("main: invalid load-extensions", "error", err, "load-extensions", *extensionsStr)
55-
return
61+
return 1
5662
}
5763

5864
// If no certificate files are specified, check for default localhost certificates
@@ -72,7 +78,7 @@ func main() {
7278
})
7379
if err != nil {
7480
logger.Error("main: error creating duckdb connector", "error", err)
75-
return
81+
return 1
7682
}
7783
defer func() {
7884
err = connector.Close()
@@ -84,20 +90,27 @@ func main() {
8490
ttl, err := time.ParseDuration(*ttlStr)
8591
if err != nil {
8692
logger.Error("main: invalid cache-ttl", "error", err)
87-
return
93+
return 1
8894
}
8995

90-
db, err := query.New(ctx, connector,
96+
queryOptions := []query.OptionFunc{
9197
query.WithMaxConnections(*poolSize),
9298
query.WithMaxCacheEntries(*maxCacheEntries),
9399
query.WithMaxCacheBytes(*maxCacheBytes),
94100
query.WithTTL(ttl),
95101
query.WithLogger(logger),
96102
query.WithFunctionBlocklist(functionBlocklist),
97-
)
103+
}
104+
if functionAllowlist.set {
105+
queryOptions = append(queryOptions, query.WithFunctionAllowlist(query.FunctionAllowlistOptions{
106+
Include: functionAllowlist.values,
107+
}))
108+
}
109+
110+
db, err := query.New(ctx, connector, queryOptions...)
98111
if err != nil {
99112
logger.Error("main: error creating query DB", "error", err)
100-
return
113+
return 1
101114
}
102115
defer db.Close()
103116

@@ -113,7 +126,7 @@ func main() {
113126
)
114127
if err != nil {
115128
logger.Error("main: error creating server", "error", err)
116-
return
129+
return 1
117130
}
118131
logger.Warn("DuckDB Server permits all HTTP and WebSocket origins for compatibility; enforce an outer origin or CSRF policy before exposing it to untrusted browsers")
119132

@@ -129,13 +142,16 @@ func main() {
129142
"ttl": ttl,
130143
"max_cache_bytes": *maxCacheBytes,
131144
"load_extensions": *extensionsStr,
145+
"function_blocklist": *functionBlocklistStr,
146+
"function_allowlist": functionAllowlist.String(),
147+
"allowlist_configured": functionAllowlist.set,
132148
}
133149
logger.Info("DuckDB Server configuration", "config", config)
134150

135151
extensions, err := db.GetExtensions(ctx)
136152
if err != nil {
137153
logger.Error("main: error getting extensions", "error", err)
138-
return
154+
return 1
139155
}
140156

141157
logger.Info("DuckDB Server Extensions", "extensions", extensions)
@@ -163,6 +179,7 @@ func main() {
163179
}
164180
if err != nil {
165181
logger.Error("main: error running HTTP server", "error", err)
166-
return
182+
return 1
167183
}
184+
return 0
168185
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Function allowlist inventories
2+
3+
When changing the reviewed inventories in this package:
4+
5+
- Treat the DuckDB release bundled by the `duckdb-go` version in [go.mod](../../go.mod) as the source of truth. Inspect that tag's function registrations, parser rewrites, and macro bodies; use `duckdb_functions()` as a classification cross-check, not as a generated allowlist.
6+
- Keep names lowercase, sorted, deduplicated, and grouped by the serialized `function_name`. Audit side effects, volatility, resource I/O, dynamic SQL or dispatch, and type or macro collisions. Leave uncertain names out.
7+
- Update the catalog exemptions, reviewed macros, and collision allowlists in `functionset_test.go` only when the matching DuckDB source justifies the exception. Verify parser-generated operators and syntax helpers with `json_serialize_sql` and executable SQL because some have no catalog row.
8+
- Keep `CoreExtensions` aligned with the bundled DuckDB release's core-extension roster, with an explicit inventory entry even when both groups are empty. Review external extensions against the exact revision pinned by DuckDB's descriptor; generated `extension_entries.hpp` data is not exhaustive.
9+
- For initial inventories and DuckDB upgrades, verify installed extension revisions and diff `duckdb_functions()` before and after loading each available extension in a fresh database, including new overloads of existing names. Treat statically linked baseline entries, dependency-loaded names classified under another extension, and lazy catalog-scoped or proprietary registrations as source-audit exceptions, not catalog-equality failures.
10+
- Keep each extension's source pin and `Compute`/`Elevated` arrays together in `<extension>.go`, using the DuckDB extension ID for the filename (for example, `unity_catalog.go`).
11+
- Put reviewed local computation or embedded static data with no resource or state effects in `Compute`; it is enabled by `DefaultFunctions`. Put resource, mutation, dynamic dispatch, catalog/session inspection, current-time, and source-limited runtime-verified names in `Elevated`. Treat catalog volatility as elevated unless exact pinned source proves the function is pure by argument. Classify a shared name by its most capable overload because validation does not bind signatures.
12+
- Update the pinned counts and classification table in [README.md](../../README.md) with the inventories. Document source-only or runtime-only limitations, especially MotherDuck's non-exhaustive proprietary runtime snapshot, and keep loading, autoloading, replacement scans, `ATTACH`, settings, and other non-function mechanisms outside the function-group claim.
13+
- As a secondary compatibility check, compare the functions emitted by [Mosaic SQL](../../../../mosaic/sql/src/index.ts). Do not copy `aggregateNames` or exports wholesale: they can be stale or include unsafe macros and non-function syntax.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package functionset
2+
3+
// These inventories match DuckDB 1.5.5 at d8cdaa33fda8df955cc76ef58a280f68f4cd43fa.
4+
var autocompleteComputeFunctions = [...]string{
5+
"check_peg_parser",
6+
}
7+
8+
var autocompleteElevatedFunctions = [...]string{
9+
"disable_peg_parser",
10+
"enable_peg_parser",
11+
"sql_auto_complete",
12+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package functionset
2+
3+
// Source: https://github.com/duckdb/duckdb-avro/tree/f9d590297485f0318f480372c70bdd852826e258
4+
var avroComputeFunctions = [...]string{}
5+
6+
var avroElevatedFunctions = [...]string{
7+
"read_avro",
8+
}

0 commit comments

Comments
 (0)