跳到主要内容
版本:5.x

安全

简介​

NOTE

本页概述了使用 Filament 时的安全注意事项。许多单独的功能都有自己的特定安全建议,例如文件上传、富文本编辑器、内联可编辑列等。使用任何 Filament 功能时,请务必阅读该功能的完整文档,包括其中包含的任何安全警告。

Filament是一个强大的框架,它使开发者能够广泛控制组件的配置和呈方渲染。这种灵活性是设计出来的——开发者可以使用配置方法(如 url()、icon()、html() 等)做强大的事情。然而,这意味着 Filament 信任你传递给这些方法的值,你有责任确保任何用户提供的数据在到达 Filament 之前都经过适当的验证和净化。

本页涵盖了使用 Filament 构建应用时的关键安全考虑因素,包括授权、输入验证和 HTML 净化。

授权​

资源授权​

Filament 自动检查 Laravel 模型策略 上的 资源 上的标准 CRUD 操作。当资源模型存在策略时,Filament 将检查 viewAny()、create()、update()、view()、delete() 等方法,然后才允许访问相应的页面和操作。

然而,Filament 的自动授权仅涵盖这些内置资源操作。你添加的任何自定义功能(自定义操作、自定义页面、自定义 Livewire 组件、API 端点或其他业务逻辑)都必须经过你的授权。除了它提供的标准 CRUD 操作之外,Filament 无法了解你的应用的授权要求。

授权及 Livewire 请求周期​

Filament 会在每一次 Livewire 请求中重新执行权限验证——无论是页面初次加载,还是随后的任何更新操作(如搜索、筛选、分页、调用操作或表单交互)。这意味着,如果用户在使用面板期间权限发生了变更,那么他们的下一次交互将依据当前的策略状态进行验证,而不是依据组件首次挂载时的策略状态。

这一机制适用于 Filament 提供的所有 Livewire 组件:

  • 资源页面(ListRecords、CreateRecord、EditRecord、ViewRecord、ManageRelatedRecords)—— 资源级别的 Resource::canAccess() 检查(以及父级资源的检查,若存在)会通过 CanAuthorizeResourceAccess trait 在每次请求时重新执行。针对特定页面及记录的检查(如 canEdit($record)、canView($record)、canCreate() 以及带参数的 canAccess(['record' => ...]))也会通过各页面类型的 hydrate() 方法在每次请求时重新执行,这与现有的 mount() 阶段调用 $this->authorizeAccess() 的逻辑相呼应。
  • 自定义面板页面(任何继承自 Filament\Pages\Page 的类,包括 SettingsPage、认证页面、仪表盘、集群页面等)—— 页面的 canAccess() 方法会通过 CanAuthorizeAccess trait 在每次请求时重新执行。
  • 关联管理器 (Relation managers) —— canViewForRecord($ownerRecord, $pageClass) 检查会通过 Filament\Resources\RelationManagers\Concerns 下的 CanAuthorizeAccess trait 在每次请求时重新执行。由于初始挂载 (mount) 受父级页面渲染阶段过滤器的控制,该 trait 仅注册了 hydrate 阶段的检查,以避免在首次请求时发生重复调用。
  • Widget —— 静态 canView() 检查会通过 Filament\Widgets\Concerns 下的 CanAuthorizeAccess trait 在每次请求时重新执行。与关联管理器类似,父级仪表盘的渲染阶段过滤器负责处理初始挂载的控制逻辑。
  • 多租户页面(RegisterTenant、EditTenantProfile)—— 它们的 canView() 检查会通过 hydrate() 方法在每次 Livewire 请求时重新执行,这与现有的 mount() 阶段检查逻辑一致。

面板级访问权限(canAccessPanel)由面板的 Authenticate 中间件强制执行,该中间件会在每次 HTTP 请求(包括 Livewire 更新)时运行。因此,如果用户在会话期间失去了面板访问权限,他们会在中间件层被拦截,甚至在检查组件级授权之前就会被拒绝。

在 Filament 面板上构建自定义 Livewire 组件时,请注意有几个 Livewire 操作会在 Filament 的授权钩子触发之前执行:

  • 公共属性会先从请求负载中反序列化(即 Livewire 的 “synth” 步骤),然后才会运行你的钩子。
  • boot() 和 boot{TraitName}() 生命周期钩子会在授权之前触发。
  • 在初始挂载时,用户的 mount() 方法体会在 trait 级的 mount{TraitName} 钩子之前运行。
  • 针对特定属性的 hydrate{PropertyName}() 钩子会在 Filament 的水合(hydrate)阶段授权之后触发,但在请求进入更新或渲染阶段之前完成。

实际上,这意味着即使授权最终会导致请求终止,这些早期钩子中的逻辑依然会执行。Filament 会在响应渲染或调用任何更新方法之前终止请求,因此未经授权的数据绝不会返回给用户;但在终止之前,可能会发生服务器端的副作用(例如用于解析记录的数据库查询、在 SELECT 时触发的审计日志条目、自定义钩子中分发的事件等)。

如果你的组件执行了某些不应针对未授权用户进行的重要操作(如触发事件、写入数据库、调用外部服务),请务必将这些操作放在 Filament 授权已触发之后运行的方法或钩子中(例如,在显式调用 $this->authorizeAccess() 之后的 mount() 方法体中,或者在通过 wire:click 调用的操作方法中——这些方法总是在授权后运行)。请避免将此类操作放在 boot() 或针对特定属性的水合钩子中。

行内可编辑列​

诸如 ToggleColumn、TextInputColumn、SelectColumn 和 CheckboxColumn 等行内可编辑表格列,在保存更改前不会检查模型策略,而只会检查该列的 disabled() 状态。如果你需要限制哪些用户可以编辑这些列,请结合自定义的授权逻辑使用 disabled() 方法。更多详情,请参阅各类可编辑列的文档。

自定义 Action​

在创建自定义 Action时,你需要负责对其进行授权。Filament 提供了 visible()、hidden() 和 authorize() 方法来辅助实现这一点,但你必须主动使用它们——系统不会自动应用这些设置。如果某项操作会修改数据或执行敏感操作,请务必确保对其进行了授权。

测试授权​

你的应用应包含一套全面的测试用例,以验证授权机制在所有入口点均得到正确执行——这不仅涵盖 Filament 的资源页面,还包括任何自定义操作、自定义页面、Livewire 组件、API 路由及其他功能。Filament 提供了测试辅助工具,用于断言操作、页面和资源在不同用户角色下的行为是否符合预期。

切勿仅依赖 Filament 内置的策略检查。应将其视为一种有益的辅助层,并始终通过测试来验证授权规则是否在全流程中得到有效执行。

验证用户输入​

Filament 的许多配置方法都支持接收闭包,以便返回动态值。诸如 url()、icon()、html() 等方法的设计初衷就是为了提供灵活性,让开发者能够构建丰富且动态的界面。然而,当传递给这些方法的值源自用户输入或不可信的数据库内容时,你有责任对其进行适当的验证和净化。

例如,列(Column)、条目(Entry)和操作(Action)上的 url() 方法会根据你提供的值渲染出一个 <a href="..."> 标签。如果你未经验证就传入源自用户输入的 URL,像 javascript:alert(document.cookie) 这样的恶意值可能会被渲染为可点击的链接,从而导致 XSS(跨站脚本攻击)漏洞。因此,在将 URL 传递给 Filament 之前,务必确保其使用安全的协议,如 http 或 https。

Filament 提供了一个 Str::sanitizeUrl() 辅助函数:如果 URL 不包含协议(即相对路径)或使用 http/https 协议,该函数会返回其 URL;对于其他情况,则返回 null。在检查协议之前,该函数会处理浏览器在解析 href 值时会自动还原的各种混淆手段——包括 HTML 实体引用(如数字形式的 &#9;/&#x09; 和像 &Tab;/&NewLine;/&colon; 这样的名称)、百分号编码的控制字符(%09、%0A)、嵌入的原始控制字符与空白字符(\t、\n、\r、NUL 字节)以及大小写混合的协议名称——因此,像 "\tJaVa\nScRiPt:alert(1)" 或 "java&#x09;script:alert(1)" 这样的值会被拒绝。如果通过检查,函数将返回未经修改的原始输入值;该辅助函数绝不会重写 URL。

use Filament\Tables\Columns\TextColumn;
use Illuminate\Support\Str;

TextColumn::make('website')
->url(fn (string $state): ?string => Str::sanitizeUrl($state))

You can call the helper anywhere a URL is being passed to a Filament configuration method (url(), image(), icon() when given a URL, openUrlInNewTab() callbacks, and so on). Internally Filament already runs every file URL it emits from components like FileUpload and SpatieMediaLibraryFileUpload through this helper.

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:

TextColumn::make('contact')
->url(fn (string $state): ?string => Str::sanitizeUrl(
$state,
allowedSchemes: ['http', 'https', 'mailto', 'tel'],
))

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 for non-standard rendering contexts — the safety analysis assumes the URL will be placed in an HTML attribute like href, where the browser performs a single HTML-entity decode and strips whitespace/control characters before parsing the scheme. If your code applies additional transformations to the return value before rendering (for example, calling urldecode() and then 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.

If you need a stricter allowlist (for example, only your own domains), wrap the helper:

TextColumn::make('website')
->url(function (string $state): ?string {
$sanitized = Str::sanitizeUrl($state);

if (blank($sanitized)) {
return null;
}

$host = parse_url($sanitized, PHP_URL_HOST);

return in_array($host, ['example.com', 'cdn.example.com'], true)
? $sanitized
: null;
})

同样地,ColorColumn 和 ColorEntry 组件会将它们的状态渲染为 background-color CSS 声明。Filament 会使用 Str::sanitizeCssColor() 辅助函数处理每个值,该函数仅允许以下格式:十六进制颜色(如 #rgb、#rgba、#rrggbb、#rrggbbaa)、纯 CSS 颜色关键字(如 red),以及内容不包含 CSS 元字符的函数式颜色表示法(如 rgb()、rgba()、hsl()、hsla()、hwb()、lab()、lch()、oklab()、oklch() 和 color())。任何其他内容——例如 red;position:fixed;inset:0;background-image:url(//attacker)——都会被拒绝,且相应的 CSS 声明会被忽略,从而防止存储的值注入额外的 CSS。当你根据不可信输入构建颜色样式时,也可以在任何地方自行调用 Str::sanitizeCssColor();如果输入合法,它将返回原始值,否则返回 null。

RichEditor 在处理由存储内容生成的 CSS 时也遵循同样的原则:文本颜色标记会通过 Str::sanitizeCssColor() 处理颜色值;网格布局区块则会将列数转换为整数,然后再将其插入到 style 属性中。这一点至关重要,因为用于净化富文本内容的 HTML 净化器会允许 style 属性通过,但不会解析其中的 CSS 代码,因此任何输出到 style 字符串中的值都必须在插入前进行净化处理。

icon() 方法接受 Blade 图标名(如 heroicon-o-user)或图像 URL(任何包含 / 字符的字符串)。图标名称字符串会通过 Blade 的图标系统进行解析,而 URL 字符串在渲染为 src 属性之前会经过转义处理。不过,如果传入用户输入的无效图标名称,会导致渲染错误;因此,如果图标值由用户控制,你仍应根据已知的白名单对其进行验证。

诸如 extraAttributes()、extraInputAttributes()、extraCellAttributes() 以及其他 extra*Attributes() 之类的方法,会将它们的值直接渲染到 HTML 中,而不会进行转义处理。这是有意为之的设计,因为这些方法常用于传递 Alpine.js 指令和 Livewire 属性,而这些内容不应被转义。然而,如果将用户可控的数据作为属性名或属性值传入,攻击者可能会突破 HTML 属性的限制并注入任意标记,从而导致 XSS 攻击。请务必确保传入这些方法的任何动态值都经过验证,或来自可信数据源。

通常情况下,每当向 Filament 配置方法传入用户可控的数据时,都应像在 Blade 模板中直接渲染该数据那样保持高度警惕。

HTML 净化​

当在 TextColumn 和 TextEntry 等组件上通过 html() 或 markdown() 等方法渲染 HTML 内容时,Filament 会自动使用 Symfony 的 HtmlSanitizer 组件对输出内容进行净化。此过程会移除 <script> 标签等潜在危险元素,从而有助于防范 XSS 攻击。

默认净化配置​

Filament 在 Laravel 服务提供者中使用如下默认配置,将 HtmlSanitizerConfig 注册为作用域绑定:

use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;

(new HtmlSanitizerConfig)
->allowSafeElements()
->allowRelativeLinks()
->allowRelativeMedias()
->allowAttribute('class', allowedElements: '*')
->allowAttribute('data-color', allowedElements: '*')
->allowAttribute('data-cols', allowedElements: '*')
->allowAttribute('data-col-span', allowedElements: '*')
->allowAttribute('data-from-breakpoint', allowedElements: '*')
->allowAttribute('data-id', allowedElements: '*')
->allowAttribute('data-type', allowedElements: '*')
->allowAttribute('style', allowedElements: '*')
->allowAttribute('width', allowedElements: 'img')
->allowAttribute('height', allowedElements: 'img')
->withMaxInputLength(500000)

Filament 的富文本编辑器在内部使用 data-* 属性来实现诸如文本颜色、网格布局、合并标签、提及(mentions)和自定义区块等功能。style 属性对于支持字体颜色、文本高亮和图像尺寸调整等富文本格式化功能是必不可少的。然而,这意味着像 background: url(...)(可能触发外部 HTTP 请求)或 position: fixed(可能创建用于钓鱼攻击的覆盖层)这样的 CSS 属性将不会被过滤掉。

如果你的应用需要渲染来自不可信用户的 HTML 内容,建议考虑限制默认配置。

自定义净化器​

由于 HtmlSanitizerConfig 已绑定到服务容器中,你可以在服务提供者中使用 extend() 方法来修改默认配置,而无需完全替换它。

添加允许的属性​

若要允许通过净化器处理额外的属性,请扩展其配置:

use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;

public function register(): void
{
$this->app->extend(
HtmlSanitizerConfig::class,
fn (HtmlSanitizerConfig $config): HtmlSanitizerConfig => $config
->allowAttribute('data-custom', allowedElements: '*'),
);
}

限制允许的属性​

若要移除 Filament 默认允许的属性,请使用 dropAttribute():

use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;

public function register(): void
{
$this->app->extend(
HtmlSanitizerConfig::class,
fn (HtmlSanitizerConfig $config): HtmlSanitizerConfig => $config
->dropAttribute('style', '*'),
);
}

NOTE

移除 Filament 富文本编辑器所依赖的属性(例如 data-color、data-cols、data-id 或 style)可能会导致富文本渲染异常。请务必在充分了解这些属性对 Filament 组件的影响后再进行限制。

完全替换净化器配置​

如果你需要完全掌控,可以在服务提供者中完全重新绑定 HtmlSanitizerConfig:

use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;

public function register(): void
{
$this->app->scoped(
HtmlSanitizerConfig::class,
fn (): HtmlSanitizerConfig => (new HtmlSanitizerConfig)
->allowSafeElements()
->allowRelativeLinks()
->allowRelativeMedias()
->allowAttribute('class', allowedElements: '*')
->withMaxInputLength(500000),
);
}

请参考 Symfony HtmlSanitizer 文档,查看配置选项的完整列表。

在 Blade 视图中净化​

在你自己的 Blade 视图中输出富文本内容(来自富文本编辑器或 Markdown 编辑器)时,你需要负责对其进行净化。你可以使用 Filament 提供的 sanitizeHtml() 字符串辅助函数:

{!! str($record->content)->sanitizeHtml() !!}

切勿将 {!! $content !!} 用于未经清洗的用户内容。如果需要将 Markdown 渲染为 HTML,请链式调用辅助函数:

{!! str($record->content)->markdown()->sanitizeHtml() !!}

面板访问​

默认情况下,在本地环境中,所有 App\Models\User 记录都可以访问 Filament 面板。在生产环境中,你必须在 User 模型中实现 FilamentUser 契约(contract)并定义 canAccessPanel() 方法,以控制哪些用户可以登录。有关详细信息,请参阅用户文档。

如果应用包含多个面板(例如管理面板和面向用户的面板),请确保 canAccessPanel() 方法检查 $panel 参数,并针对每个面板返回相应的结果。

多因素认证​

Filament 支持通过 TOTP 应用和电子邮件验证码进行多因素认证,但该功能默认处于禁用状态。MFA 仅在 Filament 面板的认证流程中强制执行——如果你的应用包含其他认证路径(如比 API 路由或非 Filament 登录页面),除非单独实现,否则这些路径将不会强制执行 MFA。

模型属性暴露​

Filament 通过 Livewire 的模型绑定向 JavaScript 公开所有非 $hidden 模型属性。这对于动态表单功能是必要的,并且只有具有相应表单字段的属性实际上是可编辑的 - 这不是批量分配漏洞。但是,如果你的模型包含不应在浏览器中可见的敏感属性(例如 API 密钥或内部标志),你应该将它们添加到模型的 $hidden 属性中,或者使用“编辑”或“查看”页面上的 mutateFormDataBeforeFill() 方法将其删除。有关更多详细信息,请参阅资源文档。

文件上传及富文本编辑器附件​

Filament 的 FileUpload 和 RichEditor 组件各有其安全考量——如果配置不当,上传的文件名、存储可见性、允许的文件类型以及由客户端控制的文件路径都可能被滥用。相关指南可在各组件的文档中找到:

将 Livewire 文件上传限制为仅限 Schema 组件​

所有使用了 InteractsWithSchemas trait 的 Livewire 组件都会暴露 Livewire 的 _startUpload 和 _finishUpload RPC 方法。这是因为该 trait 整合了 Livewire 的 WithFileUploads 功能,从而允许 FileUpload 和 MarkdownEditor 等 Schema 组件使用 Livewire 标准的文件上传机制。默认情况下,这些 RPC 方法接受针对任意 Livewire 属性名称的上传请求——它们不会检查该属性是否对应于组件 Schema 中实际定义的文件上传字段。这意味着,任何能访问该页面的攻击者都可以篡改 Livewire 请求,向使用了 InteractsWithSchemas 的页面上的任意属性路径上传文件,即使该页面根本没有显示任何上传字段。

如果你的 Livewire 组件可被你不希望上传任意文件的用户访问(例如未经验证的页面,或 Schema 中不包含上传字段的页面),请添加 RestrictsFileUploadsToSchemaComponents trait。添加后,如果上传的目标属性未映射到组件 Schema 中注册的 FileUpload 字段(或任何支持文件附件的字段),_startUpload 和 _finishUpload 方法将终止并返回 403 响应:

use Filament\Schemas\Concerns\InteractsWithSchemas;
use Filament\Schemas\Concerns\RestrictsFileUploadsToSchemaComponents;
use Filament\Schemas\Contracts\HasSchemas;
use Livewire\Component;

class ViewProduct extends Component implements HasSchemas
{
use InteractsWithSchemas;
use RestrictsFileUploadsToSchemaComponents;

// ...
}

启用该 Trait 后,攻击者若试图篡改 Livewire 请求以将文件上传至任意属性名,请求将被拒绝;而来自 Schema 中 FileUpload 字段的合法上传则不受影响,因为它们的目标属性对应着已注册的组件。此外,支持文件附件功能的组件(如 MarkdownEditor 和 RichEditor)发起的上传,只要其目标指向已注册组件,也同样被允许。

TIP

隐藏字段不被视为可匹配的目标。如果 FileUpload 字段处于条件性隐藏状态(使用了 ->visible(false) 或类似设置),则针对其状态路径的上传请求将被拒绝——只有用户实际可见的字段才是有效的上传目标。

查询范围设置​

在构建表格、资源或自定义 Livewire 组件时,请务必确保数据库查询已根据当前用户的权限进行了适当的范围限制(scope)。Filament 的资源系统默认使用返回所有记录的 Eloquent 查询;因此,你需要负责应用适当的查询范围——既可以通过表格上的 modifyQueryUsing() 方法来实现,也可以通过重写资源类中的 getEloquentQuery() 方法来实现——从而确保用户只能访问其有权查看的记录。

例如,在多租户应用中,如果忘记将查询限制在当前租户范围内,用户可能会看到其他租户的数据。如果你使用的是 Filament 内置的多租户(tenancy)功能,资源的查询范围会自动受到限制;但对于你自己构建的任何自定义查询、操作(Action)或页面,则必须手动设置查询范围。