See how monospace compares to other vendors in security performance
Summary
The SSRF protection on Directus's file-import-from-URL feature can be bypassed using the address 0.0.0.0. While 127.0.0.1 and other internal addresses are denied, 0.0.0.0 is not added to the blocklist. On Linux and macOS, connecting to 0.0.0.0 reaches localhost, so an authenticated user with file-upload rights can make the server fetch internal services and retrieve the response as a downloadable file (full-read SSRF).
Affected Versions
- Affected: Directus <= 12.0.0 (confirmed on directus/directus:latest, v11.17.3, with default configuration) - Patched: Directus >= 12.0.0
Details
Directus uses a deny-list config, IMPORTIPDENYLIST, whose default value is ['0.0.0.0', '169.254.169.254'].
The issue is in how api/src/request/is-denied-ip.ts processes this list. When it encounters the entry 0.0.0.0, it treats it as a special keyword meaning "block all local network interfaces," but it never blocks the literal address 0.0.0.0 itself. The handler sets the network-interface flag and skips to the next entry without adding 0.0.0.0 to the blocklist.
What actually gets blocked is the loopback subnet 127.0.0.0/8 (from the lo interface) plus whatever addresses are assigned to the machine's network interfaces. The address 0.0.0.0 is not inside 127.0.0.0/8; it belongs to the separate 0.0.0.0/8 range. So a request to http://0.0.0.0:8055/ passes the blocklist check as "allowed."
At the OS level, however, connecting to 0.0.0.0 reaches localhost, functionally equivalent to 127.0.0.1. The same gap applies to the IPv6 unspecified address ::. As a result, the SSRF protection is bypassed.
Impact
An authenticated user with create permission on directusfiles (file-upload rights) can make the server issue requests to its own localhost via the /files/import endpoint. The response body is stored as a downloadable file, making this a full-read SSRF. On bare-metal or single-host deployments, this can reach databases, caches, and internal APIs bound to localhost. This bypass defeats the protections tracked as CVE-2026-35409 and CVE-2024-46990.
Summary
When response caching is enabled (CACHEENABLED=true), the cache-key derivation in api/src/utils/get-cache-key.ts includes only version, path, query, and accountability.user (plus a conditional ip). Authorization context beyond user (share, role, roles, admin, app, policies) is not part of the key.
For share tokens this is load-bearing. Directus's share-authentication flow (api/src/services/shares.ts:100-105) issues a JWT without an id claim, so api/src/utils/get-accountability-for-token.ts never assigns accountability.user, leaving it null (the default from create-default-accountability.ts). Every share token, and every anonymous request, therefore reduces to user: null in the cache-key input. Two different shares (or an anonymous request and a share token) requesting the same URL with the same query produce identical cache keys. The first request populates the bucket with a permission-filtered response; subsequent hits from unrelated shares or anonymous clients receive that payload without any permission re-evaluation.
This is the web-cache pattern "authorization-dependent response cached under an unsegmented key" (cache key collision / missing authorization context in cache key, CWE-524 and CWE-639). Two adjacent read populations collide:
- Share to share: Share A populates the cache, Share B reads Share A's scoped response. - Share to anonymous (and the reverse): any unauthenticated client hitting the same URL retrieves cached share-scoped data without presenting any token.
Affected
- Config required: CACHEENABLED=true (any store: memory, redis, memcached) plus at least one active directusshares row. This is not a default-on bug: CACHEENABLED ships as false. The cache is documented as a production performance setting, so operators who enable it are the ones affected.
Vulnerability class
- CWE-524: Use of Cache Containing Sensitive Information - CWE-639: Authorization Bypass Through User-Controlled Key (the key here is the derived cache key, not the URL) - OWASP API3:2023: Broken Object Property Level Authorization
Impact
- Cross-share confidentiality breach. Any share's filtered response can be served to a holder of a different share token, or to an anonymous request, that hits the same URL and query. Shares are advertised as a mechanism to distribute scoped, read-only access to specific items; this bug makes every cached share response readable by any other share-token holder who can reach the URL (the item-detail admin UI uses a predictable pattern). - Anonymous request can read share data. Anonymous requests also compute user=null. An anonymous client hitting /items/articles?fields= after a share request has populated the cache receives the share's scoped payload with zero authentication. - Password-protected share, derivative effect. Password protection lives only at shares.login (JWT issuance). Once any share has populated the cache for a URL, an anonymous or alternate-share request to that URL retrieves the cached payload without exchanging the share password. This is the same cache-key collision surfacing as a password-protection bypass symptom, not a distinct mechanism. - Persistence. The leak persists for the CACHETTL window (commonly 5 to 30 minutes). CACHEAUTOPURGE clears on mutating writes to cached collections but does not purge per-user or per-share. With CACHESTORE=redis (common in production) the poisoned bucket survives server restarts. - No write impact. The bypass is read-only.
Scope of the leak depends on the share's permission surface. A share with no backing role (directusshares.role = null, the default) collapses visibility to the primary key, so the cache leaks only PKs. A share backed by a role with broader field access (the intended production setup for distributing useful content) leaks the full content that role can see on the scoped item.
Summary
Directus' GraphQL endpoints (/graphql and /graphql/system) did not deduplicate resolver invocations within a single request. An authenticated user could exploit GraphQL aliasing to repeat an expensive relational query many times in a single request, forcing the server to execute a large number of independent complex database queries concurrently, multiplying database load linearly with the number of aliases. The existing token limit on GraphQL queries still permitted enough aliases for significant resource exhaustion, while the relational depth limit applied per alias without reducing the total number executed. Rate limiting is disabled by default, meaning no built-in throttle prevented this from causing CPU, memory, and I/O exhaustion that could degrade or crash the service. Any authenticated user, including those with minimal read-only permissions, could trigger this condition.
Fix
A request-scoped resolver deduplication mechanism was introduced and applied broadly across all GraphQL read resolvers, both system and items endpoints. When multiple aliases in a single request invoke the same resolver with identical arguments, only the first call executes; all subsequent aliases share its result. This eliminates the amplification factor regardless of how many aliases a query contains.
Impact
- Service degradation or outage: Concurrent complex database queries exhaust the connection pool and server resources, affecting all users - Low privilege required: Any authenticated user, including those with read-only access to a single collection, can trigger this condition - Linear scaling: Impact scales with the number of aliases and depth of relational queries - Compounded by concurrency: Multiple simultaneous requests multiply the effect further
Summary A Server-Side Request Forgery (SSRF) protection bypass has been identified and fixed in Directus. The IP address validation mechanism used to block requests to local and private networks could be circumvented using IPv4-Mapped IPv6 address notation.
Details Directus implements an IP deny-list to prevent server-side requests to internal/private network ranges. The validation logic failed to normalize IPv4-Mapped IPv6 addresses (e.g., the IPv6 representation of 127.0.0.1) before checking them against the deny-list. Because the deny-list check did not recognize these mapped addresses as equivalent to their IPv4 counterparts, an attacker could bypass the restriction while the underlying HTTP client and operating system still resolved and connected to the intended private target.
This has been fixed by adding a normalization step that converts IPv4-Mapped IPv6 addresses to their canonical IPv4 form prior to validation.
Impact An authenticated user (or an unauthenticated user if public file-import permissions are enabled) could exploit this bypass to perform SSRF attacks against internal services on the same host (databases, caches, internal APIs) or cloud instance metadata endpoints (e.g., AWS/GCP/Azure IMDS).
Summary
An open redirect vulnerability exists in the login redirection logic. The isLoginRedirectAllowed function fails to correctly identify certain malformed URLs as external, allowing attackers to bypass redirect allow-list validation and redirect users to arbitrary external domains upon successful authentication.
Details
A parser differential exists between the server-side URL validation logic and how modern browsers interpret URL path segments containing backslashes. Specifically, certain URL patterns are incorrectly classified as safe relative paths by the server, but are normalized by browsers into external domain references.
This is particularly impactful in SSO authentication flows (e.g., OAuth2 providers), where an attacker can craft a login URL that redirects the victim to an attacker-controlled site immediately after successful authentication, without any visible indication during the login process.
Impact
- Phishing: Users may be silently redirected to attacker-controlled sites impersonating legitimate services after authenticating. - Credential/token theft: The redirect can be chained to capture OAuth tokens or authorization codes. - Trust erosion: Users lose confidence in the application after being redirected to unexpected domains post-login.
Summary
Directus' TUS resumable upload endpoint (/files/tus) allows any authenticated user with basic file upload permissions to overwrite arbitrary existing files by UUID. The TUS controller performs only collection-level authorization checks, verifying the user has some permission on directusfiles, but never validates item-level access to the specific file being replaced. As a result, row-level permission rules (e.g., "users can only update their own files") are completely bypassed via the TUS path while being correctly enforced on the standard REST upload path.
Impact
- Arbitrary file overwrite: Any authenticated user with basic TUS upload permissions can overwrite any file in directusfiles by UUID, regardless of row-level permission rules. - Permanent data loss: The victim file's original stored bytes are deleted from storage and replaced with attacker-controlled content. - Metadata corruption: The victim file's database record is updated with the attacker's filename, type, and size metadata. Privilege escalation potential: If admin-owned files (e.g., application assets, templates) are stored in directusfiles, a low-privilege user could replace them with malicious content.
Workaround
Disable TUS uploads by setting TUSENABLED=false if resumable uploads are not required.
Credit
This vulnerability was discovered and reported by bugbunny.ai.
Summary
Directus's Single Sign-On (SSO) login pages lacked a Cross-Origin-Opener-Policy (COOP) HTTP response header. Without this header, a malicious cross-origin window that opens the Directus login page retains the ability to access and manipulate the window object of that page. An attacker can exploit this to intercept and redirect the OAuth authorization flow to an attacker-controlled OAuth client, causing the victim to unknowingly grant access to their authentication provider account (e.g. Google, Discord).
Impact
A successful attack allows the attacker to obtain an OAuth access token for the victim's third-party identity provider account. Depending on the scopes authorized, this can lead to: - Unauthorized access to the victim's linked identity provider account - Account takeover of the Directus instance if the attacker can authenticate using the stolen credentials or provider session
Patches
This issue has been addressed by adding the Cross-Origin-Opener-Policy: same-origin HTTP response header to SSO-related endpoints. This header instructs the browser to place the page in its own browsing context group, severing any reference the opener window may hold.
Workarounds
Users who are unable to upgrade immediately can mitigate this vulnerability by configuring their reverse proxy or web server to add the following HTTP response header to all Directus responses: Cross-Origin-Opener-Policy: same-origin
Summary
Directus is vulnerable to an Open Redirect via the redirect query parameter on the /admin/tfa-setup page. When an administrator who has not yet configured Two-Factor Authentication (2FA) visits a crafted URL, they are presented with the legitimate Directus 2FA setup page. After completing the setup process, the application redirects the user to the attacker-controlled URL specified in the redirect parameter without any validation.
This vulnerability could be used in phishing attacks targeting Directus administrators, as the initial interaction occurs on a trusted domain.
Credits Discovered by Neo by ProjectDiscovery (https://neo.projectdiscovery.io/)
Summary
Aggregate functions (min, max) applied to fields with the conceal special type incorrectly return raw database values instead of the masked placeholder. When combined with groupBy, any authenticated user with read access to the affected collection can extract concealed field values, including static API tokens and two-factor authentication secrets from directususers.
Details
Fields marked with conceal are protected by payload processing logic that replaces real values with a masked placeholder on read. This protection works correctly for standard item queries, but aggregate query results are structured differently, operations are nested under their function name rather than appearing as flat field keys. The masking logic does not account for this nested structure, causing it to silently skip concealed fields in aggregate responses and return their raw values to the client.
Impact
- Account Takeover An authenticated attacker can harvest static API tokens for all users, including administrators, enabling immediate authentication as any account without credentials.
- 2FA Bypass TOTP seeds stored in directususers can similarly be extracted, allowing an attacker to bypass two-factor authentication for any account.
Summary
When GRAPHQLINTROSPECTION=false is configured, Directus correctly blocks standard GraphQL introspection queries (schema, type). However, the serverspecsgraphql resolver on the /graphql/system endpoint returns an equivalent SDL representation of the schema and was not subject to the same restriction. This allowed the introspection control to be bypassed, exposing schema structure (collection names, field names, types, and relationships) to unauthenticated users at the public permission level, and to authenticated users at their permitted permission level.
Impact
Administrators who set GRAPHQLINTROSPECTION=false to hide schema structure from clients would have had a false sense of security, as equivalent schema information remained accessible via the SDL endpoint without authentication.
Credit
This vulnerability was discovered and reported by bugbunny.ai.
Describe the Bug
In Directus, when a Flow with the "Webhook" trigger and the "Data of Last Operation" response body encounters a ValidationError thrown by a failed condition operation, the API response includes sensitive data. This includes environmental variables, sensitive API keys, user accountability information, and operational data.
This issue poses a significant security risk, as any unintended exposure of this data could lead to potential misuse.
!Image !Image !Image
To Reproduce
Steps to Reproduce: 1. Create a Flow in Directus with: - Trigger: Webhook - Response Body: Data of Last Operation 2. Add a condition that is likely to fail. 3. Trigger the Flow with any input data that will fail the condition. 4. Observe the API response, which includes sensitive information like: - Environmental variables ($env) - Authorization headers - User details under $accountability - Previous operational data.
Expected Behavior: In the event of a ValidationError, the API response should only contain relevant error messages and details, avoiding the exposure of sensitive data.
Actual Behavior: The API response includes sensitive information such as: - Environment keys (FLOWSENVALLOWLIST) - User accountability (role, user, etc.) - Operational logs (currentpayments, $last), which might contain private details.
Summary
The search query parameter allows users with access to a collection to filter items based on fields they do not have permission to view. This allows the enumeration of unknown field contents.
Details
The searchable columns (numbers & strings) are not checked against permissions when injecting the where clauses for applying the search query. This leads to the possibility of enumerating those un-permitted fields.
PoC
- Create a collection with a string / numeric field, configure the permissions for the public role to not include the field created - Create items with identifiable content in the not permitted field - Query the collection and include the field content in the search parameter - See that results are returned, even tho the public user does not have permission to view the field content
Impact
This vulnerability is a very high impact, as for example Directus instances which allow public read access to the user avatar are vulnerable to have the email addresses, password hashes and potentially admin level access tokens extracted. The admin token and password hash extraction have a caveat, as string fields are only searched with a lower cased version of the search query.
Summary There's some tools that use Directus to sync content and assets. Some of those tools use HEAD method, like Shopify, to check the existence of files. Although, when making many HEAD requests at once, at some point, all assets are being served as 403.
Details When I was investigating this issue, I have found that after the burst of HEAD requests, the amount of sockets held on Agent on NodeHttpHandler was always equal to STORAGECLOUDMAXSOCKETS making it impossible to have new connections causing assets to be inaccessible.
After looking into this issue on AWS SDK I found that if the stream is requested, it needs to be consumed otherwise will hang forever. And as can be seen here the stream is not consumed.
The timeouts set here had no noticeable effect on tests made.
PoC This can be easily reproduced with the following steps: - setup AWS S3 storage - set STORAGECLOUDMAXSOCKETS: "50" (this value is lower than default for easier reproduction) - upload a file to your project - run this file (Replace the the file ID with the one you just uploaded): ts import axios from "axios";
async function start() { Array.from({ length: 400 }, (, i) => { axios .head( "http://localhost:8055/assets/e536aa35-3a81-4fa9-b856-3780584d38d8" ) .then(() => console.log("✅")) .catch((e) => console.log("⛔", e.response?.status || e.code || e.message) ); }); }
start();
Here's an example:
https://github.com/user-attachments/assets/29d65bf0-5637-478f-a215-083c2ded3753
Impact This causes denial of assets for all policies of Directus, including Admin and Public.
Summary Since the user status is not checked when verifying a session token a suspended user can use the token generated in session auth mode to access the API despite their status.
Details There is a check missing in verifySessionJWT to verify that a user is actually still active and allowed to access the API. Right now one can extract the session token obtained by, e.g. login in to the app while still active and then, after the user has been suspended continue to use that token until it expires.
PoC Create an active user Log in with that user and note the session cookie Suspend the user (and don't trigger an /auth/refresh call, as that invalidates the session Access the API with Authorization: Bearer <token>
Impact This weakens the security of suspending users.
Summary When making many malformed transformation requests at once, at some point, all assets are being served as 403.
Details When I was investigating this issue, I have found that after a burst of malformed asset transformation requests, the amount of sockets held on Agent on NodeHttpHandler was always equal to STORAGECLOUDMAXSOCKETS making it impossible to have new connections causing assets to be inaccessible.
After looking into this issue on AWS SDK I found that if the stream is requested, it needs to be consumed otherwise will hang forever. And as can be seen here the stream is not consumed, because sharp will throw an error on the invalid arguments. For example ?height=xyz
The timeouts set here had no noticeable effect on tests made.
PoC This can be easily reproduced with the following steps: - setup AWS S3 storage - set STORAGECLOUDMAXSOCKETS: "50" (this value is lower than default for easier reproduction) - upload a file to your project - run this file (Replace the the file ID with the one you just uploaded): ts import axios from "axios";
async function start() { Array.from({ length: 400 }, (, i) => { axios .get( "http://localhost:8055/assets/e536aa35-3a81-4fa9-b856-3780584d38d8?width=100&height=XYZ" ) .then(() => console.log("✅")) .catch((e) => console.log("⛔", e.response?.status || e.code || e.message) ); }); }
start();
Here's an example:
https://github.com/user-attachments/assets/7f5a6f51-1c51-4d4d-aa4f-c4953e91714c
Impact This causes denial of assets for all policies of Directus, including Admin and Public.
Summary If there are two overlapping policies for the update action that allow access to different fields, instead of correctly checking access permissions against the item they apply for the user is allowed to update the superset of fields allowed by any of the policies.
E.g. have one policy allowing update access to fielda if the id == 1 and one policy allowing update access to fieldb if the id == 2. The user with both these policies is allowed to update both fielda and fieldb for the items with ids 1 and 2.
Details Before v11, if a user was allowed to update an item they were allowed to update the fields that the single permission, that applied to that item, listed. With overlapping permissions this isn't as clear cut anymore and the union of fields might not be the fields the user is allowed to update for that specific item.
The solution that this PR introduces is to evaluate the permissions for each field that the user tries to update in the validateItemAccess DB query, instead of only verifying access to the item as a whole. This is done by, instead of returning the actual field value, returning a flag that indicates if the user has access to that field. This uses the same case/when mechanism that is used for stripping out non permitted field that is at the core of the permissions engine.
As a result, for every item that the access is validated for, the expected result is an item that has either 1 or null for all the "requested" fields instead of any of the actual field values. These results are not useful for anything other than verifying the field level access permissions.
The final check in validateItemAccess can either fail if the number of items does not match the number of items the access is checked for (ie. the user does not have access to the item at all) or if not all of the passed in fields have access permissions for any of the returned items.
Impact This is a vulnerability that allows update access to unintended fields, potentially impacting the password field for user accounts.
Directus is a real-time API and App dashboard for managing SQL database content. Prior to 11.17.0, the PATCH /files/{id} endpoint accepts a user-controlled filenamedisk parameter. By setting this value to match the storage path of another user's file, an attacker can overwrite that file's content while manipulating metadata fields such as uploadedby to obscure the tampering. This vulnerability is fixed in 11.17.0.
Directus is a real-time API and App dashboard for managing SQL database content. Prior to 11.17.0, Directus stores revision records (in directusrevisions) whenever items are created or updated. Due to the revision snapshot code not consistently calling the prepareDelta sanitization pipeline, sensitive fields (including user tokens, two-factor authentication secrets, external auth identifiers, auth data, stored credentials, and AI provider API keys) could be stored in plaintext within revision records. This vulnerability is fixed in 11.17.0.
Summary
A timing-based user enumeration vulnerability exists in the password reset functionality. When an invalid reseturl parameter is provided, the response time differs by approximately 500ms between existing and non-existing users, enabling reliable user enumeration.
Details
The password reset endpoint implements a timing protection mechanism to prevent user enumeration; however, URL validation executes before the timing protection is applied. This allows an attacker to distinguish between valid and invalid user accounts based on response timing differences.
Impact
This vulnerability violates user privacy and may facilitate targeted phishing attacks by allowing attackers to confirm the existence of user accounts.
Security Advisory: Open Redirect in Directus SAML Authentication
Summary
An open redirect vulnerability exists in the Directus SAML authentication callback endpoint. The RelayState parameter is used in redirects without proper validation against an allowlist of permitted domains.
Vulnerability Description
During SAML authentication, the RelayState parameter is intended to preserve the user's original destination. However, while the login initiation flow validates redirect targets against allowed domains, this validation is not applied to the callback endpoint. This allows an attacker to craft a malicious authentication request that redirects users to an arbitrary external URL upon completion.
The vulnerability is present in both the success and error handling paths of the callback.
Impact
- Phishing: Users can be redirected to attacker-controlled sites that mimic legitimate login pages - Credential theft: Chained attacks may leverage the redirect to capture OAuth tokens or authorization codes - Trust erosion: Users may lose confidence in the application's security posture
This vulnerability can be exploited without authentication.
Summary
A vulnerability exists in the file update mechanism which allows an unauthenticated actor to modify existing files with arbitrary contents (without changes being applied to the files' database-resident metadata) and / or upload new files, with arbitrary content and extensions, which won't show up in the Directus UI.
Details
Directus exposes the CRUD operations for uploading or handling files under the /files route.
The endpoint handler is responsible for updating an existing file identified by the provided primary key specified through the pk parameter. Primary keys are UUID values such as /files/927b3abf-fb4b-4c66-bdaa-eb7dc48a51cb. Here the filenamedisk value is never sanitized, it's possible to pass a path containing traversal sequences (../) through it, but a fully arbitrary file write is not possible in case the "local" storage handler is used. (Other storage implementations haven't been checked during the research process). The packages/storage-driver-local/src/index.ts file defines two relevant functions: write and fullpath.
The write method uses the fullPath method to create the absolute path for the to-be-created file. The join method is used to create the final path string. As the fullPath method uses join to create a relative path starting with the separator to be added under the download dir, this call normalizes the path and further upwards traversal is not possible during the write operation. With that being said, it is still possible, to make the system "ignore" the temp prefix given to the file, resulting in an arbitrarily named file being placed in the upload folder.
As a summary for the vulnerability:
- It is possible, to change the contents of an existing file, as an existing UUID can be specified as the file name - The metadata won't change, so the mime type cannot be modified - This also makes the changes happen "silently", without directus knowing about the changes - A new, previously non-existent file can be created with arbitrary contents - The file won't show up in on the Directus UI, it can only be seen through other means (such as shell access) - An extension MUST be defined for the file to be modified - This prevents us from uploading executables or malware with no extensions, but these wouldn't be executable either way
Recommendations for fixing the vulnerability can be found in later chapters.
Requirements
As providing a primary key is required for successful exploitation, at least one asset with a known UUID must be available for an attacker. This can usually be achieved by browsing an application that uses the given Directus instance to provide images.
Naturally, the instance needs to be accessible over the network used by the attacker as well.
Once network access and knowledge of at least one file UUID is available for the attacker, exploitation can be done by sending a single request.
Potential impacts
The impact of successful exploitation is highly dependent on how Directus is set up to be used by a different application. Many different configurations can be created, but the following are likely the most noteworthy:
1. Setting up a phishing site
SVGs can be used to set up very sophisticated looking pages, as it allows the embedding of HTML, CSS and scripts. The issue is once again with the default-src: none CSP settings. This setting prevents the use of CSS in the SVG file, so the created page will look strange.
While the page obviously looks strange, it's important to notice that since the domain checks out, the browser could fill out the login forms, making for a much more convincing page as shown below:
An error message can be used to make it look like an error in the system!
2. Server serves files directly from the upload directory:
In this setup, a server such ash nginx serves files in a static manner. The served files are loaded from a "public" folder made accessible through Directus as it's recommended in the files API docs. Quoting: "make a public folder and allow access to this", except the upload folder is directly served by the server:
Since the files loaded by the server are sourced directly from the file storage, the arbitrary file write might allow an attacker to upload a webshell into the folder, giving it an arbitrary file extension. As the extension checks out as a valid PHP file for instance, and the contents are correct code, an attacker can achieve unauthenticated code execution on the server.
3. Poisoning hosted files
The previous examples focused on active exploitation, but it's important to mention that the vulnerability allows for arbitrary changes in files. This can be used for many different attack primitives. Let's consider the following scenario: Directus is used not only to serve contents on a company's web page, but internally as well. Onboarding documents for new entries are hosted on the instance. Manuals with links to internal services are provided through PDF files. If the file can be accessed and modified by an attacker, it would be trivial to set up a spoofed instance which receives credentials for internal services but redirects to the original, internal service right after.
Credits
The bug was discovered by Zombor Máté, a security researcher at PCA Cyber Security (https://pcacybersecurity.com/)
Summary:
An observable difference in error messaging was found in the Directus REST API. The /items/{collection} API returns different error messages for these two cases: 1. A user tries to access an existing collection which they are not authorized to access. 2. A user tries to access a non-existing collection.
The two differing error messages leak the existence of collections to users which are not authorized to access these collections.
Details:
The following response returns an error message, when requesting a collection the user is not authorized to access.
GET /items/no-access { "errors": [ { "message": "You don't have permission to access collection \"no-access\" or it does not exist. Queried in root.", "extensions": { "reason": "You don't have permission to access collection \"no-access\" or it does not exist. Queried in root.", "code": "FORBIDDEN" } } ] }
The following response returns a different error message when requesting a collection which does not exist.
GET /items/does-not-exist { "errors": [ { "message": "You don't have permission to access this.", "extensions": { "code": "FORBIDDEN" } } ] }
Impact:
The difference in errors between non-existent collections and collections blocked by permissions leak the existence of a collection to a user which is not authorized to access this object.
Credit:
Sebastian Krause - Hackmanit GmbH
Summary
A vulnerability allows authenticated users to search concealed/sensitive fields when they have read permissions. While actual values remain masked (), successful matches can be detected through returned records, enabling enumeration attacks on sensitive data.
Details
The system permits search operations on concealed fields in the directususers collection, including token, tfasecret, password. Matching records are returned with masked values, but their presence confirms the searched value exists.
The "Recommended Defaults" for "App Access" grant users full read permissions to their role/user records, inadvertently enabling them to search for any user's tokens, TFA secrets, and password hashes. Attackers can leverage known password hashes from breach databases to identify accounts with compromised passwords.
Impact
This vulnerability enables: - Token enumeration - Verification of valid authentication tokens - Password hash matching - Identification of accounts using known compromised passwords - Information disclosure - Confirmation of sensitive value existence without viewing actual data - Increased attack surface - Default permissions automatically expose all deployments using recommended settings
The risk is particularly high for password fields, where attackers can cross-reference publicly available hash databases to identify vulnerable accounts.
Summary Directus does not properly clean up field-level permissions when a field is deleted. If a new field with the same name is created later, the system automatically re-applies the old permissions, which can lead to unauthorized access.
Details When a field is removed from a collection, its reference in the permissions table remains intact. This stale reference creates a security gap: if another field is later created using the same name, it inherits the outdated permission entry. This behavior can unintentionally grant roles access to data they should not be able to read or modify.
The issue is particularly risky in multi-tenant or production environments, where administrators may reuse field names, assuming old permissions have been fully cleared.
1. Create a collection named testcollection. 2. Add a field called secretfield. 3. Assign a role with read permissions specifically tied to secretfield. 4. Remove the secretfield from the collection. 5. Create a new field with the exact same name secretfield. 6. Notice that the previously assigned permissions are still active, granting access to the newly created field without reconfiguration.
Impact
When creating new fields with the same name as previously deleted fields it may inherit the permissions of that previously deleted field. This can potentially result in accidentally giving access to this new field in existing policies.
Summary
A stored cross-site scripting (XSS) vulnerability exists that allows users with upload files and edit item permissions to inject malicious JavaScript through the Block Editor interface. Attackers can bypass Content Security Policy (CSP) restrictions by combining file uploads with iframe srcdoc attributes, resulting in persistent XSS execution.
Details
The vulnerability arises from insufficient sanitization in the Block Editor interface when processing JSON content containing HTML elements. The attack requires two permissions: - upload files - To upload malicious JavaScript files - edit item - To create or modify content with the Block Editor
Attack Vector:
1. JavaScript File Upload: Attackers upload a malicious JavaScript file via the files endpoint, obtaining a file ID accessible through the assets directory
2. Block Editor Exploitation: Using a JSON field with Block Editor interface, attackers inject raw HTML containing an iframe with srcdoc attribute that references the uploaded file
3. CSP Bypass: The iframe srcdoc technique circumvents existing CSP protections by creating a new document context that loads the uploaded script
The payload is injected through direct API manipulation (PATCH request) to bypass client-side validation, targeting the Block Editor's paragraph data structure within the JSON content field.
Impact
This vulnerability enables: - Persistent XSS - Malicious scripts execute whenever affected content is viewed - Session hijacking - Access to authentication tokens and cookies of users viewing the content - Administrative compromise - If administrators view infected content, their elevated privileges can be exploited - CSP bypass - Demonstrates ineffective security controls, potentially affecting other protections - Data exfiltration - Ability to steal sensitive information displayed in the application - Phishing attacks - Injection of convincing fake login forms or malicious redirects
Summary The Comment feature has implemented a filter to prevent users from adding restricted characters, such as HTML tags. However, this filter operates on the client-side, which can be bypassed, making the application vulnerable to HTML Injection.
Details The Comment feature implements a character filter on the client-side, this can be bypassed by directly sending a request to the endpoint.
Example Request:
PATCH /activity/comment/3 HTTP/2 Host: directus.local
{ "comment": "<h1>TEST <p style=\"color:red\">HTML INJECTION</p> <a href=\"//evil.com\">Test Link</a></h1>" }
Example Response:
json { "data": { "id": 3, "action": "comment", "user": "288fdccc-399a-40a1-ac63-811bf62e6a18", "timestamp": "2023-09-06T02:23:40.740Z", "ip": "10.42.0.1", "useragent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36", "collection": "directusfiles", "item": "7247dda1-c386-4e7a-8121-7e9c1a42c15a", "comment": "<h1>TEST <p style=\"color:red\">HTML INJECTION</p> <a href=\"//evil.com\">Test Link</a></h1>", "origin": "https://directus.local", "revisions": [] } }
Example Result:
!Screenshot 2023-09-06 094536
Impact
With the introduction of session cookies this issue has become exploitable as a malicious script is now able to do authenticated actions on the current users behalf.
Summary When sharing an item, user can specify an arbitrary role. It allows user to use a higher-privileged role to see fields that otherwise the user should not be able to see.
Details Specifying role on share should be available only for admins. The current flow has a security flaw.
Each other role should allow to share only in the context of the same role. As there is no role hierarchy in Directus, it is impossible to tell which role is higher or lower, so only admins should be able to specify the role for share.
Optionally, instead of specifying a role, shareer should be able to specify which fields (limited to fields shareer sees) are available on shared item. Similarily to import.
shareer - a person that creates a share link to item
PoC 1. Create a collection with a secret field. 2. Create role A that sees the secret field 3. Create role B that does not see the secret field, but can use share feature. 4. Create item with secret field filled. 5. Use account with role B to share the object as role A and gain unauthorized access to secret value.
Here's video example: https://www.youtube.com/watch?v=DbV4IxbWzN4 I had to upload it to YouTube, because GitHub allows only 10MB videos.
Impact Impacted are instances that use the share feature and have specific roles hierarchy and fields that are not visible for certain roles.
Summary When setting WEBSOCKETSGRAPHQLAUTH or WEBSOCKETSRESTAUTH to "public", an unauthenticated user is able to do any of the supported operations (CRUD, subscriptions) with full admin privileges.
Details Accountability for unauthenticated WebSocket requests is set to null, which used to be "public permissions" until the Permissions Policy update which now defaults that to system/admin level access. So instead of null we need to make use of createDefaultAccountability() to ensure public permissions are used for unauthenticated users.
PoC 1. Start directus with bash WEBSOCKETSENABLED=true WEBSOCKETSGRAPHQLAUTH=public WEBSOCKETSRESTAUTH=public
2. Subscribe using GQL or REST or do any CRUD operation on a user created collection (system tables are not reachable with crud) gql subscription { directususersmutated { key event data { id email firstname lastname password } } } or json { "type": "items", "action": "read", "collection": "yourcollectionname" } 3a. Open up the data studio as any user. Observe how the subscriber gets notified on each page navigation (because the users lastpage gets updated, the password fields is properly redacted here)
3b. Observe receiving all available items from the yourcollectionname collection.
Impact
This impacts any Directus instance that has either WEBSOCKETSGRAPHQLAUTH or WEBSOCKETSRESTAUTH set to public allowing unauthenticated users to subscribe for changes on any collection or do REST CRUD operations on user defined collections ignoring permissions.
Impact If you're relying on blocking access to localhost using the default 0.0.0.0 filter this can be bypassed using other registered loopback devices (like 127.0.0.2 - 127.127.127.127)
Workaround You can block this bypass by manually adding the 127.0.0.0/8 CIDR range which will block access to any 127.X.X.X ip instead of just 127.0.0.1.
Summary Unauthenticated user can access credentials of last authenticated user via OpenID or OAuth2 where the authentication URL did not include redirect query string.
For example: - Project is configured with OpenID or OAuth2 - Project is configured with cache enabled - User tries to login via SSO link, but without redirect query string - After successful login, credentials are cached - If an unauthenticated user tries to login via SSO link, it will return the credentials of the other last user
The SSO link is something like https://directus.example.com/auth/login/openid/callback, where openid is the name of the OpenID provider configured in Directus
Details This happens because on that endpoint for both OpenId and Oauth2 Directus is using the respond middleware, which by default will try to cache GET requests that met some conditions. Although, those conditions do not include this scenario, when an unauthenticated request returns user credentials. For OpenID, this can be seen here: https://github.com/directus/directus/blob/main/api/src/auth/drivers/openid.ts#L453-L459 And for OAuth2 can be seen here https://github.com/directus/directus/blob/main/api/src/auth/drivers/oauth2.ts#L422-L428
PoC - Create a new Directus project - Set CACHEENABLED to true - Set CACHESTORE to redis for reliable results (if using memory with multiple nodes, it may only happen sometimes, due to cache being different for different nodes) - Configure REDIS with redis string or redis host, port, user, etc. - Set AUTHPROVIDERS to openid - Set PUBLICURL to the the main URL of your project . For example, PUBLICURL: http://localhost:8055 - Configure AUTHOPENIDCLIENTID, AUTHOPENIDCLIENTSECRET, AUTHOPENIDISSUERURL with proper OpenID configurations - Be sure that on OpenID external app you have configured Redirect URI to http://localhost:8055/auth/login/openid/callback - Run Directus - Open the SSO link like http://localhost:8055/auth/login/openid/callback - Do the authentication on the OpenID external webpage - Verify that it you got redirected to a page with a JSON including accesstoken property - Be sure all anonymous mode windows are closed - Open an anonymous window and go to the SSO Link http://localhost:8055/auth/login/openid/callback and see you have the same credentials, even though you don't have any session because you are in anonymous mode
Impact All projects using OpenID or OAuth 2, that does not include redirect query string on loggin in users.