Tool File URL Signing#
Tool file URLs in Dify are protected by an HMAC-SHA256 signature scheme. Every URL for a tool-generated file must carry timestamp, nonce, and sign query parameters; the server rejects requests where the signature doesn't verify or the timestamp has expired.
Architecture: URI Generation and URL Binding#
Tool file signing now separates two concerns:
- URI generation β produces an origin-free
/files/...path with HMAC signature and expiry - URL binding β attaches the appropriate base URL (
FILES_URL,INTERNAL_FILES_URL, or a custom URL) based on the intended audience
This separation allows the same signed URI to be bound to different base URLs for different consumers (plugin, browser, internal services).
URI Generation (sign_tool_file_uri)#
sign_tool_file_uri(tool_file_id, extension) produces an origin-free, signed URI:
- Signed path β
/files/tools/{tool_file_id}{extension} - Payload β
"file-preview|{tool_file_id}|{timestamp}|{nonce}" - Signature β
HMAC-SHA256(SECRET_KEY, payload), base64url-encoded - Query string β
?timestamp={ts}&nonce={nonce}&sign={encoded_sign}
The nonce is 16 random bytes (hex-encoded); the timestamp is the current Unix epoch as a string.
URL Binding (bind_file_uri)#
bind_file_uri(uri, base_url) attaches a base URL to an origin-free URI:
- Absolute HTTP(S) URLs (remote files) pass through unchanged
/files/...URIs are prefixed withbase_urlto produce the final URL
Complete Signing (sign_tool_file)#
sign_tool_file(tool_file_id, extension, for_external=True) is a convenience wrapper that calls sign_tool_file_uri and bind_file_uri:
- Calls
sign_tool_file_urito generate the signed/files/tools/...URI - Calls
bind_file_uriwithdify_config.FILES_URL(whenfor_external=True) ordify_config.INTERNAL_FILES_URL(whenfor_external=False)
Verification (verify_tool_file_signature)#
verify_tool_file_signature recomputes the HMAC over the same payload and checks two conditions:
- Signatures must match byte-for-byte (constant-time via
hmac.new). current_time - timestamp β€ FILES_ACCESS_TIMEOUTβ default 300 seconds (5 minutes).
The HTTP endpoint GET /files/tools/<file_id>.<extension> calls verify_tool_file_signature on every request and raises 403 Forbidden on failure.
Related Functions in signature.py#
The same file contains two additional signing functions that use the same pattern:
| Function | Payload prefix | Endpoint |
|---|---|---|
sign_upload_file_preview_url | image-preview | /files/{id}/image-preview |
get_signed_file_uri_for_plugin | upload | /files/upload/for-plugin |
Configuration#
| Key | Default | Purpose |
|---|---|---|
SECRET_KEY | "" (auto-generated) | HMAC key shared across the API process |
FILES_ACCESS_TIMEOUT | 300 s | Maximum age of a signed URL |
FILES_URL | β | External base URL for signed file URLs |
INTERNAL_FILES_URL | β | Internal base URL (Docker-to-Docker); falls back to FILES_URL if unset |
Where Signing Functions Are Called#
| Caller | Function | Context |
|---|---|---|
message_file_utils.py:76 | sign_tool_file | Signs tool-file URLs in MessageFile records before streaming them to clients |
file_runtime.py | sign_tool_file_uri | Generates origin-free URIs for workflow file runtime; caller binds with bind_file_uri for the appropriate audience |
Known Inconsistency: Unsigned URLs in the Pipeline#
ToolFileMessageTransformer.get_tool_file_url() returns a bare, unsigned relative URL (/files/tools/{tool_file_id}{extension}) . This method is used when constructing IMAGE_LINK and BINARY_LINK messages for tools and blobs inside transform_tool_invoke_messages .
Similarly, base_app_runner.py stores the URL /files/tools/{tool_file.id} directly in a MessageFile record's url field without signing . Those unsigned URLs are later picked up by message_file_utils.py, which does call sign_tool_file at read time .
The practical effect is a two-stage pattern: tool-file URLs are often stored or passed as unsigned relative paths early in the pipeline, then signed only when being serialized for an external client response. Code paths that bypass message_file_utils.py may expose unsigned URLs that the /files/tools/ endpoint will reject with 403.
Key Files#
| File | Role |
|---|---|
api/core/tools/signature.py | Sign / verify functions |
api/controllers/files/tool_files.py | HTTP endpoint that enforces signature |
api/core/tools/utils/message_transformer.py | Transforms tool messages; get_tool_file_url returns unsigned URLs |
api/core/app/task_pipeline/message_file_utils.py | Signs tool-file URLs at serialization time |
api/core/app/workflow/file_runtime.py | Workflow file runtime; calls sign_tool_file_uri to generate origin-free URIs |
api/core/app/apps/base_app_runner.py | Stores unsigned /files/tools/ URL in MessageFile.url |