Coolify 源码中的 Laravel 安全实践:从 Mass Assignment 到字段级加密的落地全解
2026/9/5 18:49:21 网站建设 项目流程

Coolify 源码中的 Laravel 安全实践:从 Mass Assignment 到字段级加密的落地全解

【免费下载链接】coolifyAn open-source, self-hostable PaaS alternative to Vercel, Heroku & Netlify that lets you easily deploy static sites, databases, full-stack applications and 280+ one-click services on your own servers.项目地址: https://gitcode.com/GitHub_Trending/co/coolify

本文以 Coolify 仓库内置的 Laravel 安全规范文档(security.md)为主线,系统讲解九大安全实践:Mass Assignment 防护、细粒度授权、SQL 注入与 XSS 防护、CSRF 保护、认证与 API 限流、文件上传校验、密钥隔离、依赖审计以及敏感数据库字段加密。每一条规范都会结合 Coolify 的真实源码实现(RateLimiter 配置、私钥模型 等)展开,帮助你在自托管 PaaS 项目或任意 Laravel 应用中把安全规则真正落地,而不是停留在"知道这些坑"的层面。

一、Mass Assignment 防护:白名单优先

规范的核心要求是:每个 Eloquent 模型都必须显式定义$fillable(白名单)或$guarded(黑名单),绝不在接受用户输入的模型上使用$guarded = []

错误示例——全部字段可批量赋值,攻击者可借update()一次性篡改任意列:

class User extends Model { protected $guarded = []; // All fields are mass assignable }

正确示例——只暴露明确可写的字段:

class User extends Model { protected $fillable = [ 'name', 'email', 'password', ]; }

Coolify 中的落地方式

Coolify 的 app/Models 目录下所有模型都采用$fillable白名单模式,例如 PrivateKey:

protected $fillable = [ 'name', 'description', 'private_key', 'is_git_related', 'team_id', 'fingerprint', ];

值得注意的是,PrivateKey除了白名单外还配合了两道防线,这也是规范文档中后续两节的直接体现:

protected $hidden = [ 'private_key', // 序列化/API 输出时隐藏敏感列 ]; protected $casts = [ 'private_key' => 'encrypted', // 落库前自动加密 ];

白名单解决"哪些列能被写",$hidden解决"哪些列不能被读出去",encryptedcast 解决"写进去的是不是密文"。三者组合后,即使上游校验出现疏漏,敏感数据也不会以明文形式出现在响应或数据库中。

二、授权:每个动作都要过 Policy 或 Gate

规范要求:控制器中任何会修改数据的动作都必须显式授权,禁止跳过

错误示例——直接更新,未检查当前用户是否有权操作该资源:

public function update(UpdatePostRequest $request, Post $post) { $post->update($request->validated()); }

正确示例——先Gate::authorize再落库:

public function update(UpdatePostRequest $request, Post $post) { Gate::authorize('update', $post); $post->update($request->validated()); }

或者把授权逻辑放进 Form Request 的authorize()方法,让校验与授权同生命周期:

public function authorize(): bool { return $this->user()->can('update', $this->route('post')); }

Coolify 中的落地方式

Coolify 在 app/Policies 下为每种资源建立了独立 Policy,共 28 个策略类,覆盖 Application、Server、Service、数据库、S3 存储等全部资源类型。以 ApplicationPolicy 为例,update动作返回Response对象而非简单布尔值,被拒绝时能附带具体原因:

public function update(User $user, Application $application): Response { $teamId = $this->getTeamId($application); if ($teamId === null) { return Response::deny('Application team not found.'); } if ($user->isAdminOfTeam($teamId)) { return Response::allow(); } return Response::deny('You need at least admin or owner permissions to update this application.'); }

从源码结构看,授权判断统一收敛到"资源所属 team + 用户在 team 中的角色"两个维度(isAdminOfTeam),并且连deploymanageEnvironmentuploadBackup这类非 CRUD 动作也单独建模为策略方法,而不是笼统地挂在update上。此外 Http Kernel 中注册了can.create.resourcescan.update.resource等路由中间件别名,从源码结构推断,部分 Livewire 组件是通过中间件层而非控制器层做权限拦截的,属于"每层都可授权"的纵深设计。

三、SQL 注入:只做参数绑定,不拼字符串

规范要求:永远使用参数绑定,绝不把用户输入直接插值进 SQL 语句。

错误示例:

DB::select("SELECT * FROM users WHERE name = '{$request->name}'");

正确示例——Eloquent 查询构造器与whereRaw占位符:

User::where('name', $request->name)->get(); // Raw expressions with bindings User::whereRaw('LOWER(name) = ?', [strtolower($request->name)])->get();

Coolify 的数据访问几乎全部走 Eloquent 与查询构造器。以 PrivateKey::fingerprintExists 为例,即使查询条件来自数据库字段与运行时参数,也是通过where()链式方法构造的,用户可控值永远不会以字符串拼接方式进入 SQL。这条规范的工程含义可以概括为一句话:在 Laravel 中,"能用构造器就不用原生 SQL;必须用原生 SQL 时,必须带绑定数组"

四、XSS 防护:默认转义输出

规范:Blade 模板中一律用{{ }}做 HTML 转义;{!! !!}只允许用于已确认可信、已预净化的内容。

错误示例:

{!! $user->bio !!}

正确示例:

{{ $user->bio }}

在 Coolify 这类自托管 PaaS 中,这条规则的特殊意义在于:用户输入面非常宽——应用名、环境变量、服务模板、Webhook 载荷都可能最终出现在界面上。从源码结构看,Coolify 还在服务端对资源名做了额外防护(如 HasSafeStringAttribute 这类 Trait 出现在模型上),与模板层转义形成双重防线。使用{!! !!}渲染第三方富文本(如 Markdown 渲染结果)前,应先经 Sanctum/Purify 类组件净化——仓库 config/purify.php 的存在说明项目已引入 HTML 净化配置,可作为白名单式净化的起点。

五、CSRF:表单必带令牌,API 用签名与限流兜底

规范:所有 POST/PUT/DELETE 的 Blade 表单必须包含@csrf;Inertia 应用中该指令自动生效。

错误示例:

<form method="POST" action="/posts"> <input type="text" name="title"> </form>

正确示例:

<form method="POST" action="/posts"> @csrf <input type="text" name="title"> </form>

Coolify 中的落地方式

在 Http Kernel 中,web中间件组显式包含VerifyCsrfToken::class,即所有 Web 会话路由默认强制校验 CSRF 令牌:

'web' => [ EncryptCookies::class, AddQueuedCookiesToResponse::class, StartSession::class, ShareErrorsFromSession::class, VerifyCsrfToken::class, SubstituteBindings::class, CheckForcePasswordReset::class, DecideWhatToDoWithUser::class, ],

api中间件组则不包含VerifyCsrfToken,改为 Token 认证 + 限流(见下一节)——这正是"CSRF 针对有状态会话、API 针对无状态凭证"的分层思路。Webhook 入口(routes/webhooks.php)从源码结构看走的是独立分组,依赖来源方签名/密钥校验而非 CSRF 令牌,属于对"不可交互调用方"的合理豁免。

六、限流:认证路由与 API 路由必须加 throttle

规范给出最简实现——为登录路由定义按 IP 计数的限流器:

RateLimiter::for('login', function (Request $request) { return Limit::perMinute(5)->by($request->ip()); }); Route::post('/login', LoginController::class)->middleware('throttle:login');

Coolify 的完整限流矩阵

Coolify 在 RouteServiceProvider 中把"限流"做成了一个矩阵,值得逐条拆解:

限流器限额计数维度设计意图
api每分钟config('api.rate_limit')次,api/health单独放宽至 1000用户 ID,未登录则按 IP已登录用户按身份限流,健康检查探活不占配额
5每分钟 5 次用户 ID 或 IP通用敏感动作的默认档位
feedback每分钟 3 次用户 ID 或 IP防反馈/滥用接口刷屏
login每分钟 5 次邮箱 + IP 组合同一账号不同 IP 也各自受限,防分布式撞库
two-factor每分钟 5 次会话中暂存的login.id验证码爆破防护
forgot-password每 10 分钟 3 次(按 IP)+ 每小时 3 次(按邮箱身份)双维度叠加防邮件轰炸,且对邮箱做了归一化与哈希
magic-link每分钟 5 次token + IP 的 SHA-256防 magic link 被枚举
force-password-reset每分钟 15 次用户 ID强制改密流程防抖

其中两处实现细节特别值得借鉴。其一是登录限流的 key 设计:

RateLimiter::for('login', function (Request $request) { return Limit::perMinute(5)->by((string) $request->email.'|'.auth_rate_limit_ip($request)); });

email|ip复合键意味着"同一个账号从任意 IP 尝试登录"都会被单独计数,比单纯按 IP 限流更能抵御代理池撞库。其二是forgot-password返回了限流器数组$limits),即同一请求要同时通过多个 Limit,任一超限即拒绝——这是 Laravel 对"多维度限流"的官方支持用法。

API 侧则通过 Http Kernel 的api组挂上ThrottleRequests::class.':api',与configureRateLimiting()中的api命名限流器联动,配额值来自 config/api.php 的rate_limit配置项,部署时可按环境调整而无需改代码。

七、文件上传:扩展名、MIME 与尺寸三重校验

规范要点:mimes校验扩展名、用mimetypes校验真实 MIME 类型、校验尺寸;绝不信任客户端文件名,入库时改用生成的文件名。

校验规则示例:

public function rules(): array { return [ 'avatar' => ['required', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048'], ]; }

存储时用服务端生成的文件名:

$path = $request->file('avatar')->store('avatars', 'public');

Coolify 中的落地方式

Coolify 的上传校验统一收敛在 Livewire 组件的rules()中,例如 Profile/Index 的头像上传:

'avatar' => ['required', 'image', 'mimes:jpg,jpeg,png,webp', 'max:5120', 'dimensions:max_width=6000,max_height=6000'],

Project/Edit 的项目图标上传采用完全相同的规则组合。注意这里在规范建议的基础上追加了dimensions校验——只限制文件字节数(max:5120KB)不够,一张几 KB 的 10 万像素大图同样能造成前端渲染压力,因此尺寸上限成为标准项之一。从源码结构看,这些上传组件没有使用客户端原始文件名落盘(配合store生成的随机名与publicdisk),与规范中"never trust client-provided filenames"的要求一致。

八、密钥管理:.env不入库,代码只读config()

规范:绝不提交.env;应用代码中禁止直接调用env(),一律通过config()读取。

错误示例:

$key = env('API_KEY');

正确示例——env 只在 config 文件中出现一次:

// config/services.php 'api_key' => env('API_KEY'), // In application code $key = config('services.api_key');

原因是env()依赖Dotenv在容器引导期的加载,config 缓存(php artisan config:cache)之后env()会直接抛错,而config()始终可用。Coolify 的限流器配置config('api.rate_limit')(见 RouteServiceProvider)就是这一模式的典型用例:部署方通过.env注入值,代码只读缓存后的 config 项,从而同时获得"生产环境可 config 缓存"与"密钥不硬编码"两个性质。

九、依赖审计:composer audit进 CI

规范要求周期性运行composer audit,并在 CI 中自动化,在部署前捕获已知漏洞:

composer audit

Coolify 的依赖清单见 composer.json,生产镜像构建流程(docker/production 下的 Docker 相关配置)在发版前执行依赖审计,是把这条规范从"建议"变成"门禁"的常见做法。

十、敏感字段加密:encryptedcast +$hidden双保险

规范要求:API Key、Token 类字段使用encryptedcast 存储,并将属性标记为hidden防止序列化泄露。

错误示例:

class Integration extends Model { protected function casts(): array { return [ 'api_key' => 'string', ]; } }

正确示例:

class Integration extends Model { protected $hidden = ['api_key', 'api_secret']; protected function casts(): array { return [ 'api_key' => 'encrypted', 'api_secret' => 'encrypted', ]; } }

Coolify 中的落地方式

Coolify 是"字段级加密"用得最重的 Laravel 项目之一,全仓库共有 30 余处=> 'encrypted'cast,覆盖所有会接触外部凭证的模型:

  • Application:http_basic_auth_passwordmanual_webhook_secret_github/gitlab/bitbucket/gitea全部加密;
  • CloudProviderToken:云厂商 OAuthtoken加密;
  • GitlabApp:access_tokenrefresh_tokenclient_secret三个凭证全加密;
  • EmailNotificationSettings:SMTP 主机、账号、密码以及resend_api_key均加密;
  • EnvironmentVariable:环境变量value本身加密——对 PaaS 产品而言这是核心安全资产;
  • PrivateKey:SSH 私钥encryptedcast +$hidden,与第一节呼应。

加密依赖 LaravelCrypt服务(AES-256-CBC),密钥来自.envAPP_KEY,因此**"encrypted cast + 不入代码仓库的 .env"** 才是完整闭环:数据库被拖库后得到的是密文,没有APP_KEY无法还原。

对"加密的是数组"这种复合场景,Coolify 还提供了自定义 cast EncryptedArrayCast:set()json_encode后调用Crypt::encryptString落库,get()时先解密再json_decode,并且对解密失败的历史明文行做了降级兼容(catchDecryptException后按明文 JSON 处理)——这在"存量明文数据逐步加密迁移"的场景中是一个可以直接复用的工程模板。

小结:九条规则在 Coolify 中的映射速查

规范条目Coolify 中的证据路径
$fillable白名单app/Models/PrivateKey.php、app/Models 全目录
Policy/Gate 授权app/Policies/ApplicationPolicy.php、app/Http/Kernel.php
SQL 参数绑定app/Models/PrivateKey.php 等查询构造器用法
Blade 转义 + 净化配置config/purify.php
CSRFapp/Http/Kernel.php(web 组VerifyCsrfToken
认证/API 限流app/Providers/RouteServiceProvider.php、config/api.php
上传校验app/Livewire/Profile/Index.php、app/Livewire/Project/Edit.php
密钥走 configconfig/services.php、config/app.php
依赖审计composer.json +composer audit
敏感字段加密app/Models/EnvironmentVariable.php、app/Casts/EncryptedArrayCast.php

这套文档的价值不在于罗列通用 Laravel 知识,而在于它定义了一组可被 Agent 与开发者共同执行的审查标准:白名单、授权、绑定、转义、令牌、限流、校验、隔离、审计、加密,每条都配有正反示例。Coolify 的源码则证明这些规则在承载 SSH 私钥、云凭证、环境变量等高敏资产的自托管 PaaS 场景下是完整可运行的——这也是把它作为安全 baseline 学习样本的原因。

【免费下载链接】coolifyAn open-source, self-hostable PaaS alternative to Vercel, Heroku & Netlify that lets you easily deploy static sites, databases, full-stack applications and 280+ one-click services on your own servers.项目地址: https://gitcode.com/GitHub_Trending/co/coolify

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询