Skip to main content
You are currently viewing the documentation for Filament 4.x, which is a previous version of Filament.Looking for the current stable version? Visit the 5.x documentation.

Introduction

This page provides a general overview of security considerations when using Filament. Many individual features have their own specific security recommendations documented alongside them — for example, file uploads, rich editors, inline editable columns, and more. When using any Filament feature, make sure to read the full documentation for that feature, including any security warnings it contains.
Filament is a powerful framework that gives developers extensive control over how components are configured and rendered. This flexibility is by design — developers need to be able to do powerful things with configuration methods like url(), icon(), html(), and others. However, this means that Filament trusts the values you pass into these methods, and it is your responsibility to ensure that any user-supplied data is properly validated and sanitized before it reaches Filament. This page covers key security considerations when building applications with Filament, including authorization, input validation, and HTML sanitization.

Authorization

Resource authorization

Filament automatically checks Laravel Model Policies for standard CRUD operations on resources. When a policy exists for a resource’s model, Filament will check methods like viewAny(), create(), update(), view(), delete(), and others before allowing access to the corresponding pages and actions.
By default, a missing resource policy or policy method does not deny access: Filament allows the operation unless a Gate before callback denies it. Define the policy methods your application needs, or enable strict authorization to detect missing policies and methods. Strict mode does not add authorization to custom actions, inline editable columns, or other code that does not use resource authorization. Skipping authorization on a resource bypasses its policy checks even in strict mode.
However, Filament’s automatic authorization only covers these built-in resource operations. Any custom functionality you add — custom actions, custom pages, custom Livewire components, API endpoints, or other business logic — must be authorized by you. Filament cannot know your application’s authorization requirements beyond the standard CRUD operations it provides. Resource access and record scoping are separate. A viewAny() check controls access to the resource; it does not filter its table through each record’s view() policy. Likewise, built-in bulk delete, force-delete, and restore actions check their *Any() abilities, not each record’s policy by default. Apply query restrictions for records a user must not see, and enable individual record authorization when a bulk operation needs per-record checks.

Authorization and the Livewire request lifecycle

Filament repeats its built-in authorization checks when a component mounts and on later Livewire requests. If a user’s access changes while a component is open, their next interaction is checked against their current access:
  • Resource pages repeat their page access checks. For example, edit and view pages check access to the resource, its parent resources, and the current record. A ManageRelatedRecords page checks access to the related resource or model, but you must add an owner-record check to its canAccess() method if your application requires one.
  • Custom panel pages that extend Filament\Pages\Page check canAccess().
  • Relation managers check canViewForRecord().
  • Widgets check canView().
  • Tenant registration and profile pages check canView().
Panel access is also checked by canAccessPanel() on every request, including Livewire requests.

Understanding Eloquent model restoration in Livewire

When a public Livewire property contains an Eloquent model, Livewire stores the model’s identifier in the component snapshot. On the next request, Livewire queries the database to restore it. A signed snapshot prevents a user from changing the identifier, but it does not prove that they are still allowed to access the model. Livewire does not reuse the query that originally loaded the model. Its restoration query does not apply Filament resource or table queries, and Laravel does not apply the model’s global scopes by default.

Re-querying models with resource or tenant context

On later requests, Filament restores these built-in properties using the context owned by their component:
  • Resource page $record: the page’s record resolver and resource route binding query, including nested parent scoping. These queries preserve intentional query customizations, such as allowing soft-deleted records.
  • Nested resource page $parentRecord: the parent resource route binding queries. When ancestor route parameters are available, Filament validates the full ancestor chain. Without those parameters, Filament validates a supplied parent through its immediate parent resource’s binding query and preserves its model identity; it does not reconstruct missing ancestors.
  • Tenant profile $tenant: the panel’s current tenant.
If the query can no longer find the model, Filament returns a 404 response. Any policy or component authorization check runs separately. This protection does not follow a model into another Livewire component. A relation manager or widget cannot reliably reconstruct the resource query that originally loaded a model passed to it. Filament leaves these properties to Livewire’s native restoration instead of applying a different query with all global scopes:
  • Relation manager $ownerRecord.
  • Widget $record and $parentRecord.
  • Modal table select $record.
  • Tenant registration $tenant.
These properties are not automatically re-queried against their original resource query or the model’s global scopes. You must enforce any tenant, ownership, or other record-access restrictions before using them. A request to a child component does not run the parent page’s record binding or authorization again. InteractsWithPageTable is a specific exception when accessing table data: it knows the table’s page class. For a resource page, it resolves the synthetic page’s parent and checks ancestor resource access before booting or mounting that page, using the nested resource rules above. It does not check the parent record’s view() or update() policy, or validate other uses of the widget’s own model properties. Table row actions resolve their record through the table query. Attach and associate actions resolve selected records through their configured relationship or options query. Filament does not provide either guarantee for model values passed to custom Livewire methods or action arguments.
Component authorization is separate from record scoping. A relation manager still runs canViewForRecord(), and a widget still runs canView(). Neither automatically reapplies the parent resource’s query or authorizes a passed-in record. For a relation manager, enforce owner-record restrictions in canViewForRecord(). For widget or custom component code, query and authorize records before accessing them.

Protecting your own model properties

Filament does not re-query model values that belong to your application, including:
  • model properties that you add to a custom Livewire component, custom page, or other Filament component,
  • application-owned model properties on a Livewire component embedded inside a schema,
  • model arguments passed to a lazy component’s mount() method,
  • models nested inside arrays, collections, form state, or other public properties, and
  • model values passed through custom Livewire method or action arguments.
Do not assume that these models, or the passed-in built-in properties listed above, still pass the query or authorization rules that applied when they were first loaded. For your own properties, store a scalar key instead, then query and authorize the model when you need it. You may also re-query and authorize a restored model before using it. For example, store a locked record ID and use a computed property to query and authorize the record when you access it. Include any tenant or ownership constraints your application requires in the query, and authorize the intended operation. This example checks view access:
When a relationship is queried, it uses its relationship query and the related model’s global scopes. Accessing an already-loaded relationship does not run that query again. Filament does not automatically run a policy for every related model that your code accesses.

Running code before authorization

On a later Livewire request:
  1. Livewire restores public properties. This can run an unscoped model query, Eloquent retrieval events, and callbacks such as a Livewire form object’s boot() method.
  2. For resource pages and tenant profile pages, Filament restores the context-aware properties listed above. A resource-bound model that no longer passes its query is rejected before your component lifecycle hooks run. This step does not re-query passed-in properties on relation managers, widgets, or other child components.
  3. Filament runs the component’s applicable authorization checks. Passing the query in the previous step does not mean that the model is authorized.
Some lifecycle hooks can run before step 3:
  • A page’s boot(), trait initializers, and early mount() or hydrate() code.
  • A widget or relation manager’s boot() method.
  • A lazy widget or relation manager’s placeholder and initial dehydration hooks.
Do not rely on the order of lifecycle hooks from different traits for authorization. If code must not run for an unauthorized user, authorize the user before running it. This includes database writes, events, audit logging, and external service calls. After the applicable built-in authorization check, Livewire runs per-property hydration hooks, requested property updates, public methods requested by Livewire, actions, and normal rendering. This only protects models covered by that check. For example, a custom page’s canAccess() method does not authorize each model property that you add to the page.
Filament explicitly checks widget and relation manager authorization while a lazy component is waiting to mount. Pages do not have an equivalent check during this waiting state. Do not enable Livewire lazy loading on a Filament page that relies on page authorization hooks.

Inline editable columns

Inline editable table columns such as ToggleColumn, TextInputColumn, SelectColumn, and CheckboxColumn do not check Model Policies before saving changes. They only check the column’s disabled() state. If you need to restrict who can edit these columns, use the disabled() method with your own authorization logic. See the documentation for each editable column type for more details.

Custom actions

When you create custom actions, you are responsible for authorizing them. Filament provides visible(), hidden(), and authorize() methods to help with this, but you must use them — they are not applied automatically. If an action modifies data or performs sensitive operations, always ensure it is authorized.

Testing authorization

Your application should have a comprehensive test suite that verifies authorization is enforced correctly across all entry points — not just Filament’s resource pages, but also any custom actions, custom pages, Livewire components, API routes, and other functionality. Filament provides testing helpers for asserting that actions, pages, and resources behave correctly for different user roles. If a component keeps a model between requests, test what happens when its access changes. Mount the component, change the model’s tenant, ownership, scope, or policy result, then send another request using the existing component. Assert that the requested update, action, or render is rejected. Test any authorization that protects earlier lifecycle code separately. Do not rely solely on Filament’s built-in policy checks. Treat them as a helpful layer, but always verify that your authorization rules are enforced end-to-end through testing.

Validating user input

Many Filament configuration methods accept closures that can return dynamic values. Methods like url(), icon(), html(), and others are designed to be flexible, allowing developers to build rich, dynamic interfaces. However, when the values passed to these methods originate from user input or untrusted database content, it is your responsibility to validate and sanitize them appropriately. For example, the url() method on columns, entries, and actions renders an <a href="..."> tag with whatever value you provide. If you pass a URL sourced from user input without validation, a malicious value like javascript:alert(document.cookie) could be rendered as a clickable link, leading to XSS. Always validate that URLs use a safe scheme such as http or https before passing them to Filament. Filament ships a Str::sanitizeUrl() helper that returns the URL when it is schemeless (relative) or uses the http/https scheme, and returns null for anything else. Before checking the scheme, it decodes HTML entities and rejects control characters and raw whitespace. The return value is the original input unchanged when it passes the check; the helper does not HTML-escape or rewrite the URL. Filament escapes the value when its configuration methods render it into an HTML attribute. If you render the value yourself, escape it as an HTML attribute, including its ampersands.
Use the helper when providing an untrusted URL, such as a column, entry, or action’s url(). FileUpload and SpatieMediaLibraryFileUpload already sanitize their preview, open, and download URLs; that does not authorize access to the underlying files. If you need to allow additional schemes — for example mailto: or tel: — pass them in as the second argument. The default allowlist is replaced by what you pass, so include http and https if you still want them:
Str::sanitizeUrl() is a scheme allowlist designed to prevent XSS from dangerous URL schemes. It does not:
  • check that the host belongs to a domain you control (open-redirect protection),
  • check that the URL is safe for the server to fetch (SSRF protection),
  • guarantee safety after you transform its return value. For example, if you call urldecode() before setting location.href, apply your own scheme check to the transformed value,
  • validate that an http(s) URL is reachable or trusted in any other way.
If you need any of those guarantees, layer your own check on top of the helper’s return value. For links that must stay within your application, prefer generating the URL from a named route instead of accepting a complete URL. If you accept complete URLs and restrict their hosts, use a URL parser that follows the browser URL standard. Do not use parse_url() alone for a security allowlist, since PHP and browsers can interpret the same URL differently. Similarly, the ColorColumn and ColorEntry components render their state into a background-color CSS declaration. Filament runs every value through a Str::sanitizeCssColor() helper that only allows hex colors (#rgb, #rgba, #rrggbb, #rrggbbaa), bare CSS keyword colors (like red), and the functional notations rgb(), rgba(), hsl(), hsla(), hwb(), lab(), lch(), oklab(), oklch(), and color() whose contents do not contain CSS metacharacters. Anything else — such as red;position:fixed;inset:0;background-image:url(//attacker) — is rejected and the declaration is omitted, preventing a stored value from injecting extra CSS. You can call Str::sanitizeCssColor() yourself anywhere you build a color style from untrusted input; it returns the original value when it passes and null otherwise. The RichEditor applies the same principle to the CSS it generates from stored content: text color marks route their color through Str::sanitizeCssColor(), and the grid layout blocks cast their column counts to integers before interpolating them into a style attribute. This matters because the HTML sanitizer that cleans rich content allows the style attribute through without parsing the CSS inside it, so any value emitted into a style string must be sanitized before it gets there. The icon() method expects either a Blade icon name (like heroicon-o-user) or an image URL (any string containing /). Icon name strings are resolved via Blade’s icon system, and URL strings are escaped before rendering into src attributes. However, passing an invalid icon name from user input will cause a rendering error, so you should still validate icon values against a known allowlist if they are user-controlled. Methods like extraAttributes(), extraInputAttributes(), extraCellAttributes(), and other extra*Attributes() methods render their values into HTML without escaping. This is by design, as these methods are often used to pass Alpine.js directives and Livewire attributes that must not be escaped. However, if you pass user-controlled data as attribute names or values, an attacker could break out of the HTML attribute and inject arbitrary markup, leading to XSS. Always ensure that any dynamic values passed to these methods are validated or sourced from trusted data. As a general rule: whenever you pass user-controlled data into a Filament configuration method, treat it with the same caution you would when rendering it directly in a Blade template.

HTML sanitization

When rendering HTML content via methods like html() or markdown() on components such as TextColumn and TextEntry, Filament automatically sanitizes the output using Symfony’s HtmlSanitizer component. This removes potentially dangerous elements like <script> tags to help prevent XSS attacks.

Default sanitizer configuration

Filament registers HtmlSanitizerConfig as a scoped binding in Laravel’s service container with the following default configuration:
The data-* attributes are used internally by Filament’s rich editor for features such as text colors, grid layouts, merge tags, mentions, and custom blocks. The style attribute is necessary to support rich text formatting features such as font colors, text highlighting, and image sizing. However, this means that CSS properties like background: url(...) (which can trigger external HTTP requests) or position: fixed (which can create phishing overlays) will not be stripped. If your application renders HTML content from untrusted users, you should consider restricting the default configuration.

Customizing the sanitizer

Since HtmlSanitizerConfig is bound in the service container, you can use extend() in a service provider to modify the default configuration without replacing it entirely.

Adding allowed attributes

To allow additional attributes through the sanitizer, extend the config:

Restricting allowed attributes

To remove an attribute that Filament allows by default, use dropAttribute():
Removing attributes that Filament’s rich editor depends on (such as data-color, data-cols, data-id, or style) may break rich text rendering. Only restrict attributes when you understand their impact on Filament’s components.

Replacing the sanitizer configuration entirely

If you need full control, you can rebind HtmlSanitizerConfig entirely in a service provider:
Refer to the Symfony HtmlSanitizer documentation for the full list of configuration options.

Sanitizing in Blade views

When outputting rich text content (from a rich editor or Markdown editor) in your own Blade views, you are responsible for sanitizing it. You can use Filament’s sanitizeHtml() string helper:
Never use {!! $content !!} with unsanitized user content. If you need to render Markdown as HTML, chain the helpers:

Panel access

By default, all App\Models\User records can access Filament panels in local environments. In production, you must implement the FilamentUser contract on your User model and define the canAccessPanel() method to control who can log in. See the users documentation for details. If your application has multiple panels (e.g. an admin panel and a user-facing panel), ensure that canAccessPanel() checks the $panel argument and returns the appropriate result for each one.

Multi-factor authentication

Filament supports multi-factor authentication via TOTP apps and email codes, but it is not enabled by default. MFA is enforced within the Filament panel authentication flow — if your application has other authentication paths (such as API routes or non-Filament login pages), MFA will not be enforced on those paths unless you implement it separately.

Model attribute exposure

Filament exposes all non-$hidden model attributes to JavaScript via Livewire’s model binding. This is necessary for dynamic form functionality, and only attributes with corresponding form fields are actually editable — this is not a mass assignment vulnerability. However, if your model contains sensitive attributes that should not be visible in the browser (such as API keys or internal flags), you should either add them to the model’s $hidden property or remove them using the mutateFormDataBeforeFill() method on your Edit or View page. See the resources documentation for more details.

File uploads and rich editor attachments

Filament’s FileUpload and RichEditor components both have their own security considerations — uploaded file names, storage visibility, accepted file types, and client-controlled file paths can each be abused if misconfigured. The relevant guidance lives alongside each component:

Restricting Livewire file uploads to schema components

Every Livewire component that uses the InteractsWithSchemas trait exposes Livewire’s _startUpload and _finishUpload RPC methods, because the trait composes Livewire’s WithFileUploads so that schema components like FileUpload and MarkdownEditor can use Livewire’s standard file upload mechanism. By default, those RPC methods accept uploads to any Livewire property name — they do not check whether the property corresponds to a real upload field in the component’s schemas. This means an attacker who can reach the page can tamper with a Livewire request to upload files to arbitrary property paths on any page that uses InteractsWithSchemas, even pages that do not display an upload field at all. If your Livewire component is reachable to users you do not want uploading arbitrary files (for example, an unauthenticated page, or any page whose schema does not contain an upload field), add the RestrictsFileUploadsToSchemaComponents trait. This causes _startUpload and _finishUpload to abort with a 403 response unless the upload’s target property maps to a FileUpload field (or any field that supports file attachments) registered in one of the component’s schemas:
With the trait in place, an attacker tampering with a Livewire request to upload to an arbitrary property name is rejected; legitimate uploads from your schema’s FileUpload fields continue to work because their target property matches a registered component. Uploads from components that support file attachments — such as the MarkdownEditor and RichEditor — are also allowed when they target a registered component.
Hidden fields are not considered matchable targets. If a FileUpload field is conditionally hidden (->visible(false) or equivalent), uploads to its state path are rejected — only fields the user can actually see are valid upload targets.

Scoping queries

When building tables, resources, or custom Livewire components, ensure that database queries are properly scoped to the current user’s permissions. Filament’s resource system uses Eloquent queries that return all records by default — it is up to you to apply appropriate query scopes using the modifyQueryUsing() method on your table or by overriding the getEloquentQuery() method on your resource to ensure users can only access records they are authorized to see. For example, in a multi-tenant application, forgetting to scope queries to the current tenant would allow users to see other tenants’ data. If you are using Filament’s built-in tenancy features, Filament registers a tenant global scope for tenant-aware resource models after the current tenant is identified. Ordinary Eloquent queries for those models then use the scope, including queries in custom actions and pages. You must scope queries for other models, queries made before the tenant is identified or outside the tenant-aware panel, and queries that remove the scope. See tenancy security for all limitations.