Security best practices for UDFs
Fused UDFs are flexible, but callers and shared links should be treated with the same care as any public API. The sections below cover secrets, parameters, SQL, paths, Canvas access, and optional passcodes.
Use Secrets
Never put passwords or API keys in UDF source. Add them in secrets management, then read them at runtime (for Snowflake, set names like SNOWFLAKE_USER and SNOWFLAKE_PASSWORD there—see Databases).
Do not: hardcode credentials
Do not put real passwords or keys in UDF source. Anyone with repo or UDF access can read them, and they leak in logs and diffs.
@fused.udf
def udf():
import snowflake.connector
conn = snowflake.connector.connect(
user="janedoe",
password="do-not-commit-this",
account="xy12345",
warehouse="COMPUTE_WH",
database="MY_DB",
schema="PUBLIC",
)
return conn.cursor().execute("SELECT 1").fetchone()
Do: load secrets from secrets management
Read secrets at runtime with fused.secret("NAME") or fused.secrets["NAME"] so values stay out of source control.
@fused.udf
def udf():
import snowflake.connector
import pandas as pd
conn = snowflake.connector.connect(
user=fused.secret("SNOWFLAKE_USER"),
password=fused.secret("SNOWFLAKE_PASSWORD"),
account="your_account_identifier",
warehouse="your_warehouse",
database="your_database",
schema="your_schema",
)
cursor = conn.cursor()
cursor.execute("SELECT CURRENT_VERSION()")
df = cursor.fetch_pandas_all()
cursor.close()
conn.close()
return df
Narrow parameters
Design the UDF API so callers cannot widen what the code reads or how it queries.
| Prefer | Avoid |
|---|---|
Typed arguments (limit: int, state: str) | Raw SQL strings from callers |
| A fixed path or table inside the UDF for one-off pipelines | A generic path or sql parameter (hard to validate, cache, and reason about) |
Explicit options (columns, order_by chosen from a small set) | Free-form fragments spliced into queries |
| Serializable data (JSON, CSV, numbers, …) | Untrusted pickle; eval / exec / compile on caller input |
Don’t run caller-supplied code
Parameters should be data, not code from callers.
pickle.load/pickle.loadson untrusted bytes can execute arbitrary code. Do not unpickle caller-controlled input (or unvetted files).eval/exec/compileon caller-supplied strings — Never. Same for other “run this string as code” patterns.
DuckDB / SQL patterns
- Values (IDs, filters, …) — Don’t drop them into the SQL with an f-string or
+. Write?where each value goes, then pass the real values in a list:con.execute("... WHERE x = ?", [value]). DuckDB treats those as plain data, not as extra SQL. For paths, fix or strictly validate them—see Do not expose the data source as a parameter. - Column or table names — You can’t use
?for names—only for values. Pick allowed names in code (e.g. a Pythonset), reject anything else, then build the small part of the query that contains the name.
(Same pattern in other databases; syntax may differ.)
Anti-pattern:
@fused.udf
def udf(user_id: str = "abc"):
import duckdb
path = "s3://bucket/data.parquet"
con = duckdb.connect()
return con.execute(
f"SELECT * FROM read_parquet('{path}') WHERE id = '{user_id}'"
).df()
Safe:
@fused.udf
def udf(data_type: str = "type_a"):
import duckdb
path = "s3://bucket/data.parquet"
con = duckdb.connect()
return con.execute(
"SELECT * FROM read_parquet(?) WHERE data_type = ?",
[path, data_type],
).df()
Column name from an allowed list:
@fused.udf
def udf(column: str = "data_type"):
import duckdb
path = "s3://bucket/data.parquet"
con = duckdb.connect()
allowed = {"hex", "data_type", "count"}
if column not in allowed:
raise ValueError("Unsupported column")
query = f'SELECT "{column}" FROM read_parquet(?)'
return con.execute(query, [path]).df()
Do not expose the data source as a parameter
Prefer fixing file paths and table names in the UDF body instead of taking them as arguments. That narrows what the UDF can access and keeps behavior predictable.
A path or URL argument lets callers redirect reads to any object your runtime can reach (another bucket, a different tenant’s prefix, local files, etc.) unless you maintain a strict allowlist—which is easy to get wrong. Workbench does not treat path-like parameters as overridable run arguments by default, to reduce accidental or malicious path substitution. Use fixed locations in code for normal UDFs; the looser path example below is only for internal shared utilities.
Looser surface area — path as a parameter (reasonable only for a reusable “preview any file”–style tool):
@fused.udf
def preview_parquet(path: str, number_rows_to_preview: int = 10):
import duckdb
con = duckdb.connect()
return con.execute(
"SELECT * FROM read_parquet(?) LIMIT ?",
[path, number_rows_to_preview],
).df()
Tighter surface area — path fixed inside the UDF:
@fused.udf
def udf(number_rows_to_preview: int = 10):
import duckdb
path = "s3://fused-user/my-user/my_file.parquet"
con = duckdb.connect()
return con.execute(
"SELECT * FROM read_parquet(?) LIMIT ?",
[path, number_rows_to_preview],
).df()
Predictable inputs improve safety and caching.
Scope access with Canvas
Access is managed per canvas (team vs public, share links, passcodes when enabled). Everyone who can open a canvas sees the same UDFs on it and the same rules for calling them over HTTPS.
If you only want to expose some UDFs—or a different audience—use a separate canvas: keep the public or broadly shared work on one canvas, and move UDFs that need tighter or different access to another canvas with the right shared settings.
[Experimental] Protect endpoints with Canvas passcode
You can require a canvas-wide passcode in Canvas settings so viewers must enter it before the canvas loads. UDFs shared from that canvas use the same protection when called through those shared links.

Passcode Canvas is currently in experimental mode. You need to go to Preferences > Experimental Features and enable "Canvas passcode protection".
This passcode has no character requirement and can be edited by anyone in your Fused team.
Accessing Canvas with passcode
Anyone who opens the shared canvas link is prompted for the passcode before the canvas loads.
Accessing UDFs with passcode
Opening a UDF as an API requires sending the passcode in an Authorization header:
Curl request:
curl -s \
-H 'authorization: fused-canvas-passcode <YOUR_PASSCODE>' \
"https://udf.ai/<YOUR_CANVAS_TOKEN>/my_udf.json"
Python request:
import requests
response = requests.get(
"https://udf.ai/<YOUR_CANVAS_TOKEN>/my_udf.json",
headers={"authorization": "fused-canvas-passcode <YOUR_PASSCODE>"}
)