Directus Headless CMS: Role Escalation, File Library Exposure, and the Defaults That Bite
Directus is one of the most popular open-source headless CMS platforms, sitting behind thousands of production websites, mobile apps, and IoT data flows. It's also a recurring audit finding. Permission templates that don't scale, file library exposure, API access tokens with excessive privileges, and the Flows engine's hook execution that becomes an attack vector when misused.
Founder of Valtik Studios. Pentester. Based in Connecticut, serving US mid-market.
Why Directus keeps showing up on B2B audits
We see this pattern show up on almost every engagement.
Directus is a headless CMS built on your own SQL database. You point Directus at Postgres, MySQL, SQLite, or similar. It introspects your schema and gives you an admin UI, REST API, GraphQL API, file management, user roles, and automation flows. Developers like it because it doesn't force you into its own database model. You keep control of your data.
It's deployed in production at thousands of companies. Marketing websites, product catalogs, mobile-app backends, content management for news organizations, data infrastructure for IoT projects. The platform has matured rapidly since 2020; Directus 11 shipped in late 2024 with significant refactoring.
It's also consistently misconfigured. The permission model is flexible enough that developers commonly get it wrong. The file library has its own access model separate from collections. The Flows engine (automation workflows) has hooks that can have security implications. API tokens are often over-privileged. And the default public permissions, while conservative in newer versions, still get loosened during development and never tightened back.
This post walks through the specific attack patterns we find on Directus deployments, the underlying misconfigurations. And the hardening every Directus production deployment should implement.
Attack pattern 1: Permissive public role
Directus has a built-in Public role that controls what unauthenticated users can do. In early versions, the Public role had broader defaults. Modern versions are more restrictive. But developers still modify Public role permissions during development for testing convenience. And often forget to tighten them in production.
Misconfiguration:
- Public role has
readpermission on collections containing user data, orders, or other sensitive content - Public role has
readpermission ondirectus_users(exposes all users) - Public role has
readpermission ondirectus_files(exposes file metadata) - Public role permissions are set to
$FULL_ACCESSon any collection
Enumeration:
# Directus exposes itself at /items/<collection> and /users
curl https://target.com/items/users
curl https://target.com/users
curl https://target.com/items/orders
Any response other than a 403 indicates Public role can access that data.
Real finding: a consumer app's Directus instance had Public role with read on their customers collection. The collection contained names, emails, phone numbers, addresses. A single unauthenticated curl returned 30,000+ customer records.
The fix:
- Audit Public role in Settings → Access Control → Roles → Public
- Set Public to "No Access" by default
- Explicitly grant read-only access only to truly public collections (blog posts, published articles, public pricing)
- Never grant Public access to
directus_users,directus_files, or system collections
Attack pattern 2: Role hierarchy confusion
Directus roles don't have a formal hierarchy. Each role has its own permissions. Complex deployments often have 5-15 custom roles (Admin, Editor, Manager, Customer, Premium Customer, Support, etc.) with overlapping permission matrices.
Common misconfigurations:
- A lower-privilege role accidentally has broader permissions on specific collections than intended
- Permission inheritance assumptions that don't hold (Directus doesn't inherit)
- Role hierarchy documented in code comments but not enforced in permissions
- Collections added later that get default permissions not aligned with the role matrix
Real finding: a SaaS product had roles for "Customer" and "Premium Customer." The intended access was "Customer can read their own orders, Premium can read their own orders + view premium features." Due to permission drift, "Customer" role had read on the entire premium_content collection. A Free account could access all premium content by hitting the API directly.
The fix:
- Document the intended permission matrix per role
- Test each role's permissions with API calls, not the UI
- Review permissions quarterly
- Reset permissions to the documented matrix if drift is detected
Attack pattern 3: Item-level permissions bypass
Directus supports item-level permissions via filter expressions. "this role can only read items where user_id equals the requester's ID." Powerful feature, commonly misconfigured.
Common bugs:
- Filter expression uses client-controllable data.
user_id = $CURRENT_USERis safe.role_id = $CURRENT_ROLEwhere the client controls their role claim isn't. - Filter expression missing on some operations. Read filter set correctly, but update filter is missing, allowing any authenticated user to modify any item.
- Filter expression with subtle logic bugs.
user_id = $CURRENT_USER OR is_public = true. An attacker setsis_public = trueon their own item, then reads others with the same filter pattern.
Real finding: a team collaboration tool had item-level filters for the tasks collection. Read filter: assigned_to = $CURRENT_USER. Update filter: not set (default allowed if any update permission existed). Users could reassign tasks to themselves via update, then read them. Information disclosure across teams.
The fix:
- Set item-level filters for every operation (read, update, delete) that needs them
- Test filters by logging in as different users and attempting to access each others' data
- Use
$CURRENT_USERsystem variable for ownership-based filters - Don't trust client-provided role or team claims without validating against the user record
Attack pattern 4: File library permission model
Directus has a separate permission model for files. Files are referenced from collections. But the files themselves have independent access control via the directus_files system collection.
Common misconfigurations:
- Files uploaded via frontend user interfaces inherit the uploader's folder permissions, which are often too broad
- Public folders intended for specific public assets but accidentally holding private files
- File URL format (
/assets/) is guessable if file IDs are sequential or predictable - Asset URL permissions not tied to the requesting user's session
Real finding: a document-sharing SaaS stored user-uploaded PDFs in Directus. File-level permissions were set to "authenticated users can read." Any logged-in user could enumerate file IDs and download every user's uploaded documents.
The fix:
- Files used in authenticated contexts should have permissions matching the referencing collection
- Use Directus's built-in file permission on specific folders
- Consider using signed URLs for sensitive assets (Directus supports this in newer versions)
- Don't expose
directus_filesto public or broad authenticated roles - Validate file access at the controller level for sensitive content
Attack pattern 5: Static tokens left in collections
Directus supports API tokens for server-to-server integration. Tokens are associated with user accounts and inherit that user's permissions.
Common misconfiguration:
- Admin-scoped tokens stored in frontend code. An application stores a token with admin permissions in frontend JavaScript for "convenience." Anyone viewing the page source can extract the token and use it.
- Tokens shared across multiple integrations. Rotation becomes impossible because rotating breaks many integrations at once.
- Tokens stored in git commits. Leaked to public repos or retained in git history.
- Tokens without expiration. Static tokens that persist indefinitely even after staff changes.
Real finding: a startup's frontend had a Directus API token in their JavaScript bundle with admin-level permissions. The token was used to fetch configuration data. Any visitor could copy the token and use it to create/modify/delete any data in the Directus instance.
The fix:
- Never put admin tokens in frontend code. Use the frontend authentication flow instead.
- Per-integration tokens with minimum-necessary permissions. Each integration gets its own dedicated user + token.
- Rotate tokens on a schedule. Quarterly at minimum.
- Monitor token usage. Directus activity logs show token usage patterns.
- Use ephemeral tokens via the authentication flow where possible, instead of static API tokens.
Attack pattern 6: Flows engine abuse
Directus Flows is an automation system. Triggers (events, webhooks, schedules) invoke operations (send email, update data, call external APIs, run custom scripts).
Security-relevant Flow patterns:
- Public-triggered Flows. Flows that accept unauthenticated webhooks can be abused for:
- External API abuse (flow calls attacker-controlled URLs)
- Data manipulation (flows that modify data based on webhook input without validation)
- Flows with elevated privileges. Flows can run with admin permissions. If their logic trusts user-provided data, the elevation becomes a vulnerability.
- Flows that send email based on input. Abuse for phishing / spam relay if input isn't validated.
Real finding: a contact form created a Flow that ran on submission. The Flow sent an email to an admin address, then created a record in a leads collection. The flow also included a secondary step that called an external API with data from the submission. Attacker noticed and sent submissions with malicious API URL data → the flow called attacker-controlled URLs with Directus's infrastructure → used for various abuse patterns (SSRF, relay attacks on other services).
The fix:
- Validate all Flow inputs. Never trust webhook payload data without explicit validation.
- Allowlist external destinations for Flows that make outbound API calls.
- Rate-limit Flow executions to prevent resource exhaustion.
- Use minimum-necessary Flow permissions. If a Flow doesn't need admin, don't give it admin.
- Review Flow changes. Directus Flows can be modified through the UI. Changes should be reviewed like code.
Attack pattern 7: Schema introspection exposure
Directus's schema introspection endpoint (GET /schema/snapshot) returns the entire database schema. All collections, all fields, all relations, all permissions. Useful for tooling. Also a detailed roadmap for attackers.
Common misconfiguration:
- Schema snapshot endpoint accessible to
Publicor broad authenticated roles - Documentation pages publishing the schema unnecessarily
The fix:
- Schema snapshot should be admin-only by default (it's in recent versions)
- Review permissions on
directus_collections,directus_fields,directus_relations
Attack pattern 8: Outdated version with known CVEs
Directus releases frequently, sometimes including security fixes. Self-hosted deployments commonly lag on updates.
Common missed patches:
- Authentication bypass CVEs (historically several)
- SQL injection in legacy query endpoints
- Prototype pollution in older Node versions
- Dependency vulnerabilities in Directus's own dependencies
The fix:
- Subscribe to Directus GitHub releases
- Update within 30 days of security release
- Plan for automatic dependency updates
- Consider Directus Cloud for managed updates
Attack pattern 9: GraphQL-specific exposures
Directus has a GraphQL endpoint at /graphql. GraphQL provides capability beyond REST:
- Deep nested queries that can be expensive
- Introspection queries that reveal schema
- Aliased fields that bypass some permission checks
- Fragment sharing that enables enumeration
Common bugs:
- GraphQL introspection enabled in production
- Query depth limits not configured. Single deep query can DoS the database
- Aliasing bypass on field-level permissions (similar to the Hasura pattern we documented)
- Authorization checks that miss GraphQL-specific query patterns
The fix:
- Disable introspection in production unless specifically needed (
SKIP_GRAPHQL_SCHEMA_INTROSPECTION=true) - Configure max query depth and complexity
- Test GraphQL queries with alias bypass patterns
- Apply the same permission model to GraphQL as to REST
Attack pattern 10: Misconfigured CORS
Directus allows CORS configuration per environment. Default settings are restrictive. Deployments frequently widen CORS for convenience.
Common misconfigurations:
- CORS set to allow any origin with credentials (
*with credentials is forbidden by browsers, but similar patterns like reflecting any Origin header happen) - CORS allows specific development origins that exist only on localhost
- CORS whitelist includes subdomains that aren't used (potential for takeover)
The fix:
- CORS should allow only specific production origins
- No localhost origins in production
- Review CORS origin allowlist regularly
The hardening checklist
If you run Directus in production, work through:
Authentication and roles
- [ ] Public role reviewed and restricted to truly public data
- [ ] Custom roles documented with intended permission matrix
- [ ] Permissions tested via API calls for each role
- [ ] Role audit performed quarterly
- [ ] Admin access limited to specific individuals
- [ ] MFA on admin accounts (configure via OAuth provider)
Permission model
- [ ] Every collection has explicit permission rules
- [ ] Item-level filters tested and working
- [ ] Filters cover all operations (read, create, update, delete)
- [ ] No collection uses
$FULL_ACCESSfor lower-privilege roles - [ ]
directus_usersanddirectus_filesnot readable by Public or broad roles
API access
- [ ] No admin-level API tokens in frontend code
- [ ] Per-integration tokens with minimum permissions
- [ ] Token rotation schedule established (quarterly)
- [ ] Token usage monitored
Files
- [ ] File permissions match content sensitivity
- [ ] File library not exposed to unauthenticated users (unless specifically public)
- [ ] Sensitive files use signed URLs or controller-level access checks
Flows
- [ ] Flow inputs validated
- [ ] External URLs in Flows use allowlists
- [ ] Flow execution rate-limited
- [ ] Flow permissions minimum-necessary
GraphQL
- [ ] Introspection disabled in production (unless needed)
- [ ] Query depth limits configured
- [ ] Aliasing and fragment attacks tested
Infrastructure
- [ ] Directus version current (quarterly updates minimum)
- [ ] Dependencies up to date
- [ ] CORS origin allowlist production-only
- [ ] Database credentials not exposed in environment files committed to git
- [ ] Backups configured and tested
Monitoring
- [ ] Activity logs retained (Directus stores them in DB, verify retention)
- [ ] Alerts on unusual patterns (mass deletions, role changes, permission modifications)
- [ ] Failed authentication attempts logged
For Directus Cloud vs self-hosted
Directus offers a managed Cloud product. Tradeoffs:
Directus Cloud:
- Managed updates, backups, infrastructure
- Monolens scaling and monitoring
- Directus team operations responsibility
- Pay for hosted compute
Self-hosted:
- Full control
- Lower marginal cost
- All responsibility for security, updates, infrastructure
- Typical deployment: Docker / Kubernetes
For most organizations, Directus Cloud is easier from a security perspective. The provider handles infrastructure-level risks. Self-hosted requires genuine operational discipline.
For small deployments
If you're running Directus for a small project:
- Use Directus Cloud or a managed deployment where possible
- Public role: No access
- Specific collections readable by Public only when they're public (blog posts, published content)
- Strong admin password, unique to this service
- Regular updates
- Activity log review monthly
This gets you from "default-insecure" to "reasonably-hardened" with minimal effort.
For enterprise deployments
Larger Directus deployments require:
- Formal role matrix documentation with change management
- Automated permission testing in CI (verify role permissions haven't drifted)
- Token rotation and management infrastructure
- Integration with enterprise identity provider (SAML, OIDC)
- Audit logging to external SIEM
- Network segmentation isolating Directus from other environments
- Backup and disaster recovery testing
- Penetration testing annually
For Valtik clients
Valtik's Directus security audits include all the patterns in this post plus:
- Complete role matrix review
- Collection-level and item-level permission testing
- API token inventory and exposure review
- Flow audit for security implications
- Schema introspection and GraphQL testing
- Infrastructure review (Docker, reverse proxy, database)
If you run Directus in production and haven't had an independent security review, reach out via https://valtikstudios.com.
The honest summary
Directus is a capable platform with a flexible permission model that requires explicit attention to configure correctly. The defaults are conservative in modern versions. The drift happens during development as permissions get loosened for testing convenience and not tightened afterward.
The path to a secure Directus deployment: document your intended permission model, implement it, test it. And review it periodically. This isn't unique to Directus. Every CMS and BaaS platform needs similar discipline. What's unique is Directus's flexibility, which creates both its value and its complexity.
Audit your deployment against the patterns above. Most find at least three applicable.
Sources
Want us to check your Directus setup?
Our scanner detects this exact misconfiguration. plus dozens more across 38 platforms. Free website check available, no commitment required.
