安全
简介
INFO
本页概述使用 Filament 时的安全注意事项。许多单独功能在各自文档中有具体安全建议——例如文件上传、富文本编辑器、行内可编辑列等。使用任何 Filament 功能时,请阅读该功能的完整文档,包括其中的安全警告。
Filament 是强大的框架,让开发者对组件的配置与渲染有广泛控制。这种灵活性是设计使然——开发者需要能用 url()、icon()、html() 等配置方法做强大的事。但这意味着 Filament 信任你传入这些方法的值,你有责任在用户提供的数据到达 Filament 之前正确校验与消毒。
本页涵盖用 Filament 构建应用时的关键安全考虑,包括授权、输入校验与 HTML 消毒。
授权
Resource 授权
Filament 会为 resources 上的标准 CRUD 操作自动检查 Laravel Model Policies。若 resource 的模型存在 policy,Filament 会在允许访问对应页面与 actions 之前检查 viewAny()、create()、update()、view()、delete() 等方法。
不过,Filament 的自动授权仅覆盖这些内置 resource 操作。你添加的任何自定义功能——自定义 actions、自定义页面、自定义 Livewire 组件、API 端点或其他业务逻辑——都必须由你自行授权。Filament 无法知晓其标准 CRUD 之外的应用授权需求。
授权与 Livewire 请求生命周期
Filament 会在每次 Livewire 请求时重新运行授权——包括初始页面加载以及之后的每次更新(搜索、筛选、分页、action 调用、表单交互)。这意味着若用户在使用面板时权限发生变化,下一次交互会按当前 policy 状态授权,而非组件首次挂载时的 policy 状态。
这适用于 Filament 附带的每个 Livewire 组件:
- Resource pages(
ListRecords、CreateRecord、EditRecord、ViewRecord、ManageRelatedRecords)— 资源级Resource::canAccess()检查(以及父 resource 的检查,若有)通过CanAuthorizeResourceAccesstrait 在每次请求时重新运行。页面级、按记录限定的检查(canEdit($record)、canView($record)、canCreate()、参数化的canAccess(['record' => ...]))通过各页面类型的hydrate()方法在每次请求时重新运行,与现有的mount()时调用$this->authorizeAccess()相对应。 - Custom panel pages(任何继承
Filament\Pages\Page的页面,包括SettingsPage、身份验证页面、仪表盘、cluster 页面)— 页面的canAccess()方法通过CanAuthorizeAccesstrait 在每次请求时重新运行。 - Relation managers —
canViewForRecord($ownerRecord, $pageClass)检查通过Filament\Resources\RelationManagers\Concerns下的CanAuthorizeAccesstrait 在每次请求时重新运行。初始挂载由父页面渲染时的筛选把关,因此该 trait 只注册 hydrate 时检查,以避免首次请求重复调用。 - Widgets — 静态
canView()检查通过Filament\Widgets\Concerns下的CanAuthorizeAccesstrait 在每次请求时重新运行。与 relation managers 一样,父仪表盘渲染时的筛选负责初始挂载把关。 - Tenancy pages(
RegisterTenant、EditTenantProfile)— 其canView()检查通过镜像现有mount()时检查的hydrate()方法,在每次 Livewire 请求时重新运行。
面板级访问(canAccessPanel)由面板的 Authenticate 中间件强制执行,该中间件在每次 HTTP 请求(包括 Livewire 更新)上运行——因此中途失去面板访问权的用户会在中间件层被拦截,不会进入组件级授权。
在 Filament 面板上构建自定义 Livewire 组件时,请注意 若干 Livewire 活动会在 Filament 授权钩子触发之前运行:
- 公共属性会在任意钩子运行之前从请求载荷反序列化(Livewire 的「synth」步骤)。
- `boot()` 与 `boot{TraitName}()` 生命周期钩子在授权之前触发。
- 在初始挂载时,用户的 `mount()` 方法体在 trait 级 `mount{TraitName}` 钩子之前运行。
- 按属性的 `hydrate{PropertyName}()` 钩子在 Filament 的 hydrate 时授权之后触发,但仍在请求进入 update 或 render 之前完成。
实际上这意味着 在这些较早钩子中发生的工作,即便授权随后会中止请求,仍会执行。Filament 在渲染响应或调用任何 update 方法之前中止,因此不会向用户返回未授权数据,但服务端副作用(解析记录的数据库查询、在 SELECT 时触发的审计日志、自定义钩子中派发的事件等)可能在中止之前发生。
若组件会做未授权用户不应发生的重要操作——派发事件、写入数据库、调用外部服务——请把这些工作放在 Filament 授权之后运行的方法或钩子中(例如在 mount() 中显式调用 $this->authorizeAccess() 之后,或在通过 wire:click 调用的 action 方法中,后者始终在授权后运行)。避免把此类工作放在 boot() 或按属性的 hydrate 钩子中。
行内可编辑列
ToggleColumn、TextInputColumn、SelectColumn、CheckboxColumn 等行内可编辑表格列在保存更改前不会检查 Model Policies,只检查列的 disabled() 状态。若需限制谁能编辑这些列,请用 disabled() 配合你自己的授权逻辑。详见各可编辑列类型的文档。
自定义 actions
创建自定义 actions 时,授权由你负责。Filament 提供 visible()、hidden() 与 authorize() 方法协助,但你必须主动使用——不会自动应用。若 action 会修改数据或执行敏感操作,务必确保已授权。
测试授权
应用应有全面的测试套件,验证所有入口都正确强制授权——不仅是 Filament 的 resource 页面,还包括自定义 actions、自定义页面、Livewire 组件、API 路由及其他功能。Filament 提供测试辅助方法,用于断言不同用户角色下 actions、页面与 resources 的行为正确。
不要仅依赖 Filament 的内置 policy 检查。把它们当作有用的一层,但务必通过测试端到端验证授权规则得到强制执行。
校验用户输入
许多 Filament 配置方法接受可返回动态值的闭包。url()、icon()、html() 等方法设计为灵活,便于构建丰富的动态界面。但当传入这些方法的值来自用户输入或不信任的数据库内容时,你有责任适当校验与消毒。
例如,列、条目与 actions 上的 url() 方法会用你提供的任意值渲染 <a href="..."> 标签。若未经校验传入来自用户输入的 URL,恶意值如 javascript:alert(document.cookie) 可能被渲染为可点击链接,导致 XSS。传给 Filament 之前,务必校验 URL 使用 http 或 https 等安全 scheme。
Filament 提供 Str::sanitizeUrl() 辅助方法:当 URL 无 scheme(相对路径)或使用 http/https 时返回该 URL,否则返回 null。检查 scheme 之前,它会处理浏览器在解析 href 时会静默还原的混淆手段——HTML 实体引用(数字如 	/	,命名如 	/
/:)、百分号编码的控制字符(%09、%0A)、内嵌的原始控制字符与空白(\t、\n、\r、NUL 字节)以及大小写混合的 scheme——因此像 "\tJaVa\nScRiPt:alert(1)" 或 "java	script:alert(1)" 的值会被拒绝。通过检查时返回值是未改动的原始输入;该辅助方法从不改写 URL。
use Filament\Tables\Columns\TextColumn;
use Illuminate\Support\Str;
TextColumn::make('website')
->url(fn (string $state): ?string => Str::sanitizeUrl($state))凡是把 URL 传给 Filament 配置方法的地方(url()、image()、传入 URL 时的 icon()、openUrlInNewTab() 回调等)都可调用该辅助方法。内部上,Filament 已对 FileUpload、SpatieMediaLibraryFileUpload 等组件发出的每个文件 URL 运行此辅助方法。
若需允许额外 scheme——例如 mailto: 或 tel:——作为第二个参数传入。默认白名单会被你传入的内容替换,因此若仍需要 http 与 https,请一并包含:
TextColumn::make('contact')
->url(fn (string $state): ?string => Str::sanitizeUrl(
$state,
allowedSchemes: ['http', 'https', 'mailto', 'tel'],
))Str::sanitizeUrl() 是用于防止危险 URL scheme 导致 XSS 的 scheme 白名单。它不会:
- 检查主机是否属于你控制的域名(开放重定向防护),
- 检查 URL 对服务器抓取是否安全(SSRF 防护),
- 保证非标准渲染上下文中的安全——安全分析假定 URL 会放在如 `href` 的 HTML 属性中,浏览器在解析 scheme 前会做一次 HTML 实体解码并去除空白/控制字符。若你的代码在渲染前对返回值做了额外变换(例如先 `urldecode()` 再设置 `location.href`),请对变换后的值自行做 scheme 检查,
- 以任何其他方式验证 `http(s)` URL 是否可达或可信。
若需要上述任一保证,请在辅助方法返回值之上叠加你自己的检查。
若需要更严格的白名单(例如仅允许自己的域名),可包装该辅助方法:
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。凡是从不信任输入构建颜色样式的地方,都可自行调用 Str::sanitizeCssColor();通过时返回原值,否则返回 null。
RichEditor 对从存储内容生成的 CSS 采用同样原则:文本颜色标记会经 Str::sanitizeCssColor() 处理颜色;网格布局块在把列数插进 style 属性前会先转为整数。这很重要,因为清理富文本的 HTML sanitizer 会放行 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。
默认 sanitizer 配置
Filament 在 Laravel 服务容器中将 HtmlSanitizerConfig 注册为 scoped 绑定,默认配置如下:
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)data-* 属性由 Filament 富文本编辑器内部用于文本颜色、网格布局、合并标签、提及与自定义块等功能。style 属性是支持字体颜色、文本高亮、图片尺寸等富文本格式所必需的。但这意味着像 background: url(...)(可能触发外部 HTTP 请求)或 position: fixed(可能创建钓鱼覆盖层)之类的 CSS 属性不会被剥离。
若应用渲染来自不信任用户的 HTML 内容,应考虑收紧默认配置。
自定义 sanitizer
由于 HtmlSanitizerConfig 已在服务容器中绑定,可在 service provider 中用 extend() 修改默认配置,而无需完全替换。
添加允许的属性
要让 sanitizer 放行额外属性,可扩展配置:
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', '*'),
);
}DANGER
移除 Filament 富文本编辑器依赖的属性(如 data-color、data-cols、data-id 或 style)可能破坏富文本渲染。仅在了解对 Filament 组件的影响后再限制属性。
完全替换 sanitizer 配置
若需要完全控制,可在 service provider 中完全重新绑定 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 契约,并定义 canAccessPanel() 方法以控制谁能登录。详见用户文档。
若应用有多个面板(例如管理面板与面向用户的面板),请确保 canAccessPanel() 检查 $panel 参数,并为每个面板返回适当结果。
多因素身份验证
Filament 支持通过 TOTP 应用与邮件验证码进行多因素身份验证,但默认未启用。MFA 在 Filament 面板身份验证流程中强制执行——若应用还有其他身份验证路径(如 API 路由或非 Filament 登录页),除非另行实现,否则这些路径不会强制 MFA。
模型属性暴露
Filament 通过 Livewire 的模型绑定,把所有非 $hidden 的模型属性暴露给 JavaScript。这对动态表单功能是必要的,且只有对应表单字段的属性才真正可编辑——这不是批量赋值漏洞。但若模型包含不应在浏览器中可见的敏感属性(如 API 密钥或内部标志),应将其加入模型的 $hidden 属性,或在 Edit/View 页面用 mutateFormDataBeforeFill() 移除。详见资源文档。
文件上传与富文本编辑器附件
Filament 的 FileUpload 与 RichEditor 组件都有各自的安全考虑——上传文件名、存储可见性、接受的文件类型以及客户端控制的文件路径,若配置不当都可能被滥用。相关指导见各组件文档:
- File upload security — 文件名保留风险与授权已有文件路径流程。
- Rich editor file attachment IDs —
data-id篡改,以及默认 provider 与spatie/laravel-medialibraryprovider 在作用域上的差异。
将 Livewire 文件上传限制为 schema 组件
每个使用 InteractsWithSchemas trait 的 Livewire 组件都会暴露 Livewire 的 _startUpload 与 _finishUpload RPC 方法,因为该 trait 组合了 Livewire 的 WithFileUploads,以便 FileUpload、MarkdownEditor 等 schema 组件使用 Livewire 标准文件上传机制。默认情况下,这些 RPC 方法接受上传到任意 Livewire 属性名——不会检查该属性是否对应组件 schemas 中的真实上传字段。这意味着能访问该页面的攻击者可篡改 Livewire 请求,向任何使用 InteractsWithSchemas 的页面上的任意属性路径上传文件,即使页面根本没有显示上传字段。
若你的 Livewire 组件可被你不希望任意上传文件的用户访问(例如未认证页面,或 schema 中不含上传字段的页面),请添加 RestrictsFileUploadsToSchemaComponents trait。这会使 _startUpload 与 _finishUpload 在上传目标属性未映射到组件 schemas 中已注册的 FileUpload 字段(或任何支持文件附件的字段)时,以 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) 或等效写法),对其 state path 的上传会被拒绝——只有用户实际能看到的字段才是有效上传目标。
限定查询作用域
构建表格、resources 或自定义 Livewire 组件时,确保数据库查询已按当前用户权限正确限定作用域。Filament 的 resource 系统默认使用返回全部记录的 Eloquent 查询——你需用表格上的 modifyQueryUsing(),或重写 resource 上的 getEloquentQuery(),应用适当查询作用域,确保用户只能访问有权查看的记录。
例如,在多租户应用中,若忘记将查询限定到当前租户,用户就能看到其他租户的数据。若使用 Filament 内置的租户功能,resources 的查询会自动限定。但你构建的任何自定义查询、actions 或页面必须手动限定作用域。