Avatar Management#
User avatars in Dify are backed by the generic UploadFile infrastructure. The Account.avatar field stores either a file UUID (pointing to an UploadFile row) or a full HTTP/HTTPS URL for externally-hosted avatars. Signed URLs for file-based avatars are generated on demand β never stored β to ensure access control is applied at read time.
Data Model#
| Store | What is persisted |
|---|---|
accounts.avatar (PostgreSQL) | File UUID or external HTTP(S) URL (max 255 chars) |
upload_files (PostgreSQL) | Metadata: tenant_id, created_by, created_by_role, storage key, MIME type |
| Storage backend (S3 / MinIO / local FS) | Actual image bytes, keyed by UUID-based path |
The avatar column is defined in api/models/account.py as Mapped[str | None]. Profile mutations (including avatar) are orchestrated by AccountProfileService, which delegates to AccountRepository.update_profile(). The repository uses short-lived SQLAlchemy sessions via session_factory.begin() rather than db.session(). On failure, the service raises account_errors.AccountNotFoundError, which the controller catches and translates to the AccountNotFound() Flask exception.
API Endpoints#
Avatar endpoints live in api/controllers/console/workspace/account.py:
PATCH /account/profile β Consolidated profile mutation endpoint that accepts any combination of name, avatar, interface_language, interface_theme, and timezone in a single request. Include only the fields you want to update. The request is validated by AccountProfilePatchPayload, which delegates to AccountProfileService.update() via application_services().accounts.profile.update(). The service uses AccountRepository.update_profile() backed by short-lived SQLAlchemy sessions (not db.session()).
POST /account/avatar β Deprecated. Accepts { "avatar": "<file-uuid-or-url>" } and persists the value to Account.avatar. Marked deprecated=True in the OpenAPI spec. For backward compatibility, this endpoint still works but now delegates to the consolidated PATCH /account/profile endpoint via _update_account_profile(). New code should use PATCH /account/profile instead. No ownership validation occurs here; the file ID is accepted at face value.
GET /account/avatar β Accepts ?avatar=<file-uuid-or-url> and resolves it to a signed URL. The controller delegates to application_services().accounts.avatar.resolve(request_context, args.avatar) :
- If the value starts with
http://orhttps://, it is returned as-is (external URL passthrough). - Otherwise, the
AccountAvatarServicefetches theUploadFilerecord viaSQLAlchemyAccountAvatarFileGatewayand performs a two-layer ownership check (see Security section below). - On success, a signed URL is generated via
file_helpers.get_signed_file_url(upload_file_id=upload_file.id)and returned in anAvatarUrlResponse.
All profile endpoints now use the @console_account_admission() decorator and RequestContext for framework-neutral account identification, replacing the older @with_current_user decorator pattern.
URL Resolution and Configuration#
Avatar signed URLs are produced by graphon.file.helpers.get_signed_file_url , which uses the same URL configuration as all other Dify file URLs. Configuration is defined in FileAccessConfig :
| Setting | Env vars | Purpose |
|---|---|---|
FILES_URL | FILES_URL, CONSOLE_API_URL | Base URL for browser-facing signed links |
INTERNAL_FILES_URL | INTERNAL_FILES_URL, SERVER_CONSOLE_API_URL | Internal URL for plugin daemon / service-to-service access |
FILES_ACCESS_TIMEOUT | FILES_ACCESS_TIMEOUT | Signed URL expiry; default 300 s |
In Docker Compose deployments, set INTERNAL_FILES_URL=http://api:5001 to allow the plugin daemon to reach avatar files over the internal network without SSL issues .
Security: IDOR Fix#
PR #35771 fixed an Insecure Direct Object Reference (IDOR) vulnerability in GET /account/avatar . Before the fix, any authenticated user could obtain a signed URL for any UploadFile by supplying an arbitrary UUID β there were no authorization checks.
The fixed endpoint enforces two checks before signing, all short-circuiting to a generic 404 NotFound (to prevent enumeration). The controller delegates avatar resolution to application_services().accounts.avatar.resolve(request_context, args.avatar), which uses AccountAvatarService backed by SQLAlchemyAccountAvatarFileGateway to perform:
- File exists β
UploadFilerecord must be present in the database. - Account ownership β
upload_file.created_by_role == CreatorUserRole.ACCOUNTandupload_file.created_by == current_user.id.
Because account avatars are account-level data (not workspace-scoped resources), users can access their avatar after switching workspaces (#40054). The account ownership check is sufficient to prevent IDOR attacks since avatars are personal to the account. PR #40055 removed the tenant ownership check that previously caused avatars to become inaccessible after workspace switching.
Storage Consistency#
Avatar files are standard UploadFile records and are subject to the same DB-storage sync concerns as all uploads. A PostgreSQL row with no corresponding file in the storage backend causes a FileNotFoundError in opendal_storage.load_stream, surfacing as a 502/404 on file-preview endpoints .
Common causes of desync:
- Out-of-band scripts deleting files from the storage directory without touching the database.
- Incomplete volume mounts during version upgrades.
Remediation β two CLI commands in api/commands/storage.py :
# Remove DB rows whose physical files are missing
docker compose exec api flask clear-orphaned-file-records
# Remove storage files with no DB row
docker compose exec api flask remove-orphaned-files-on-storage
Key Files#
| File | Role |
|---|---|
api/controllers/console/workspace/account.py | AccountProfileApi, AccountAvatarApi β PATCH/GET/POST endpoints |
api/services/account_avatar_service.py | AccountAvatarService β service layer for avatar URL resolution |
api/services/account_avatar_file_gateway.py | SQLAlchemyAccountAvatarFileGateway β avatar file access with ownership checks |
api/services/account_profile_service.py | AccountProfileService β orchestrates profile mutations |
api/repositories/account_repository.py | AccountRepository β persistence backed by short-lived SQLAlchemy sessions |
api/services/account_ports.py | Port interfaces for account services (AccountAvatarFileGateway, etc.) |
api/services/account_errors.py | AccountNotFoundError, AvatarFileNotFoundError β framework-neutral error contract |
api/services/entities/account_entities.py | AccountProfileChanges, AccountSnapshot β framework-neutral entities |
api/models/account.py | Account.avatar field definition |
api/controllers/common/fields.py | AvatarUrlResponse response model |
api/configs/feature/__init__.py | FILES_URL, INTERNAL_FILES_URL, FILES_ACCESS_TIMEOUT config |
api/commands/storage.py | DB-storage reconciliation CLI commands |