BootstrapAdmin 代码安全审计报告
审计信息
| 项目 | 内容 |
|---|---|
| 目标仓库 | BootstrapAdmin(bootstrapadmin 后台管理系统) |
| 本地路径 | /BootstrapAdmin |
| 分支/版本 | master(提交 5b39a0f7,NET Core 10.x) |
| 审计日期 | 2026-08-12 |
| 审计范围 | 全部源码(211 个 .cs、58 个 .razor)、配置文件、数据库种子脚本、CI/构建脚本 |
| 主要技术栈 | ASP.NET Core + Blazor Server + BootstrapBlazor + PetaPoco(默认 ORM)/ FreeSql / SqlSugar / EFCore + Bootstrap.Security.Blazor |
说明:默认启用的 ORM 为 PetaPoco(
ServiceCollectionExtensions.cs中AddPetaPocoDataAccessServices(),其余 ORM 均被注释)。本报告以默认路径为主,同时标注备用 ORM 中的同类问题。
漏洞总览
| # | 严重级别 | 漏洞类别 | 位置 | 影响 |
|---|---|---|---|---|
| 1 | 🔴 高危 | SQL 注入(排序字段 ORDER BY) | DefaultDataService.cs:77,84、Exceptions.razor.cs:59、Traces.razor.cs:45、ExceptionService.cs:56、TraceService.cs:66 |
认证用户可注入 SQL,拖库/篡改后台数据,威胁系统数据与应用数据 |
| 2 | 🔴 高危 | 默认/硬编码凭据 | db/*/InitData.sql、README、UserLogin.razor.cs:25 |
部署不改默认密码 → 直接获得管理员权限 |
| 3 | 🔴 高危 | 弱口令哈希算法(单轮 SHA-256) | UserService.cs:40(PetaPoco/EFCore/FreeSql/SqlSugar 全部一致) |
拖库后可离线爆破用户口令 |
| 4 | 🔴 高危 | 硬编码/可猜测 JWT 签名密钥 + 真实 OAuth 密钥泄露 | appsettings.json:52、appsettings.Development.json:33-45 |
SSO Token 伪造;第三方账号授权凭据泄露 |
| 5 | 🟠 中危 | 开放重定向(Open Redirect) | LoginController.cs:83、Utils/LoginHelper.cs:46,52、Pages/Home/Index.cs:84-85 |
登录后跳转钓鱼站,诱导用户 |
| 6 | 🟠 中危 | 未授权认证接口 + 无限制爆破 | Controllers/api/LoginController.cs:29-58 |
未授权暴力破解口令/验证码、账号枚举 |
| 7 | 🟠 中危 | 短信验证码弱点 → 手机号账号接管 | TencentSMSProvider.cs:67,120、DefaultSMSProvider.cs:85、SMSLogin.razor.cs:29-59 |
4 位验证码可爆破;短信轰炸 |
| 8 | 🟠 中危 | SSRF(未授权) | Controllers/api/GiteeController.cs:31-95 |
利用服务端发起对 gitee.com / appveyor.com 任意路径请求并回显 |
| 9 | 🟡 低危 | XSS(MarkupString 渲染健康检查数据) | HealthCheckDetails.razor.cs:33-38 |
若健康检查数据含用户可控字符串,可反射 XSS |
| 10 | 🟡 低危 | 敏感信息外发 + 硬编码监控凭据 | appsettings.json:32-47、Logging:Cloud:Url |
错误日志(含堆栈/路径)外发第三方;DSN/ApiKey 硬编码 |
| 11 | 🟡 低危 | CI/测试脚本硬编码数据库口令 | scripts/appveyor/appveyor.test.ps1:9,12 |
SA/root 口令 Password12! 公开 |
| 12 | 🟡 低危 | 登录 CSRF(缺防伪令牌) | Controllers/LoginController.cs:35-66 |
可跨站强制他人登录攻击者账号 |
| 13 | 🟡 低危 | 商业许可证 Token 入库 | keys/Longbow.lic |
商业授权 token 泄露(非认证凭据) |
| 14 | 🟡 低危 | 访问控制依赖宽松子串匹配(设计层面) | AdminService.cs:50-54 |
菜单 URL ACL 用 Contains 子串匹配,数据层无角色校验 |
详细发现
🔴 1. SQL 注入 —— 排序字段 SortName 未校验直接拼入 ORDER BY(默认 PetaPoco 路径)
严重级别:高危 类别:SQL 注入 影响:系统数据 / 用户数据 / 应用数据
漏洞点:
默认启用的 PetaPoco 数据服务把 QueryPageOptions.SortName 原样传入 ORM:
src/blazor/admin/BootstrapAdmin.DataAccess.PetaPoco/Services/DefaultDataService.cs:77var items = await db.PageAsync<TModel>(option.PageIndex, option.PageItems, option.ToFilter(), option.SortName, option.SortOrder);DefaultDataService.cs:84(不分页分支同样)var items = await db.FetchAsync<TModel>(option.ToFilter(), option.SortName, option.SortOrder);
PetaPoco 的 PageAsync<T>/FetchAsync<T> 会把排序字段原样拼接进 ORDER BY 子句(不做参数化/转义)。option.SortName 来源于 BootstrapBlazor Table 组件的排序状态,由客户端(Blazor Server SignalR 消息)提交,仓库代码中没有任何白名单/属性名校验。
后台异常日志/访问日志页的查询同样存在排序拼接:
BootstrapAdmin.Web/Components/Pages/Admin/Exceptions.razor.cs:59sortList.Add($"{options.SortName} {options.SortOrder}"); ... ExceptionService.GetAll(..., sortList);BootstrapAdmin.Web/Components/Pages/Admin/Traces.razor.cs:45(同类)BootstrapAdmin.DataAccess.PetaPoco/Services/ExceptionService.cs:56/TraceService.cs:66sql.OrderBy(string.Join(", ", sortList));
备用 ORM(SqlSugar,默认注释未启用)存在同类问题:
BootstarpAdmin.DataAccess.SqlSugar/Service/DefaultDataService.cs:47.OrderByIF(option.SortOrder != SortOrder.Unset, $"{option.SortName} {option.SortOrder}")
攻击路径: 任何已登录后台用户(含默认 User 账号),在可排序表格页提交恶意 SortName(如 (SELECT ...), 盲注/时间盲注 payload)→ 注入 ORDER BY → 拖取 Users 表口令哈希、篡改数据、添加管理员。需登录,但属于后台核心数据面,且覆盖所有使用 DefaultDataService 的表格页(用户/角色/部门/菜单/字典等 14 个管理页)。
验证: 仓库中 DefaultDataService / Exceptions / Traces 均未对 SortName 做白名单校验,直接透传。
修复建议:
- 在数据服务层对
SortName做白名单校验:仅允许TModel的真实属性名(用反射或[Sortable]属性白名单),否则回退默认排序。 - 对
sortList中的每一项同样做属性名白名单过滤后再OrderBy。 - 统一封装一个
ValidateSortName<TModel>(string)工具方法,PetaPoco / SqlSugar / FreeSql / EFCore 四个数据服务复用。
🔴 2. 默认账号 + 硬编码默认凭据
严重级别:高危 类别:硬编码账号密码 / 未授权访问 影响:系统权限
漏洞点:
- 数据库种子脚本内置弱口令默认账号,且 README 公开:
db/SQLite/InitData.sql:1-5:Admin/123789(Administrators 角色)、User/123789(普通账号)- 其余
db/MySQL、db/SqlServer、db/Oracle、db/Postgresql、db/MongoDB种子脚本同样内置 README.zh-CN.md:142-144明确公布登录口令
- 登录组件 DEBUG 分支硬编码预填:
BootstrapAdmin.Web/Components/Components/UserLogin.razor.cs:23-26#if DEBUG UserName = "Admin"; Password = "123789"; #endif
影响: 若生产部署沿用种子账号且未改密,攻击者直接用公开口令 Admin/123789 登录获得系统最高权限(Administrators 角色),进而管理全部用户、角色、菜单、字典。
修复建议:
- 移除种子脚本中的固定口令;首次启动强制修改初始密码(或生成随机密码)。
- 生产环境删除演示账号
User;关闭 DEBUG 预填。 - 对密码复杂度、首次登录改密做强约束。
🔴 3. 弱口令哈希算法(单轮 SHA-256,无密钥派生)
严重级别:高危 类别:认证/数据泄露 影响:用户数据
漏洞点:
所有数据访问层(PetaPoco/EFCore/FreeSql/SqlSugar 四个实现一致)的口令校验与保存均使用 Longbow.Security.Cryptography.LgbCryptography.ComputeHash(password, salt):
BootstrapAdmin.DataAccess.PetaPoco/Services/UserService.cs:40(校验)、:187(改密)、:250-266(创建用户)- EFCore / FreeSql / SqlSugar 三个实现同名方法(
UserService.cs:38等)
实际验证: 对种子数据计算 SHA-256("123789" + salt) 的 base64 结果与 InitData.sql 中 Admin 的 Password 值 Es7WVgNsJuELwWK8daCqufUBknCsSC0IYDphQZAiGOo= 完全一致,确认算法为:单轮 SHA-256(口令+盐拼接),无 PBKDF2 / bcrypt / scrypt / Argon2 等密钥拉伸,且盐值固定写在口令旁(数据库同列)。
影响: 一旦数据库泄露(配合 #1 SQL 注入或任意文件读取),攻击者可对高价值弱口令离线高速爆破(GPU 每秒数十亿次),普通用户口令几乎必破。
修复建议:
- 改用
Rfc2898DeriveBytes(PBKDF2-SHA256,迭代次数 ≥ 100k)或 bcrypt/Argon2id。 - 新口令入库时用新算法,老口令校验时逐步迁移(登录成功即重哈希)。
🔴 4. 硬编码/可猜测的 JWT 签名密钥 + 真实 OAuth 凭据泄露
严重级别:高危 类别:硬编码密钥 / 认证绕过 影响:系统权限 / 第三方账号
漏洞点:
- 弱 JWT 签名密钥(硬编码):
BootstrapAdmin.Web/appsettings.json:48-53"TokenValidateOption": { "Issuer": "BA", "Audience": "api", "Expires": 5, "SecurityKey": "BootstrapAdmin-V1.1" }该配置供
Bootstrap.Security用于跨域 SSO Token 的签发/校验。密钥是公开可猜测的字符串常量,若攻击者获知可伪造任意用户/管理员 SSO Token。 - 真实 OAuth 应用密钥泄露:
BootstrapAdmin.Web/appsettings.Development.json:31-45"GiteeOptions": { "ClientId": "9bfe9b95d813...", "ClientSecret": "3427f2d901...", ... } "GitHubOptions": { "ClientId": "ec53ecfe238558a0423b", "ClientSecret": "ffa759ca599d...", ... }这是真实的 Gitee/GitHub OAuth 应用凭据(非
<占位符>),已被随仓库提交而公开。攻击者可冒用该应用身份完成 OAuth 授权,或调用对应平台 API。
影响: SSO Token 伪造可绕过认证直接以任意身份(含管理员)进入系统;OAuth 密钥泄露可滥用第三方账号授权。
修复建议:
SecurityKey改用强随机密钥并从环境变量/密钥管理服务(如用户机密、KMS)注入,禁止入库。- 立即在 Gitee/GitHub 后台吊销并重置当前泄露的 ClientSecret,改为环境变量注入。
- 全仓库(含历史提交)清除
appsettings.Development.json等含真实密钥的文件;.gitignore中忽略本地密钥文件。
🟠 5. 开放重定向(Open Redirect)
严重级别:中危 类别:钓鱼/诱导 影响:用户
漏洞点:
登录成功后的跳转目标由用户可控参数直接决定,未校验是否为站内地址:
Controllers/LoginController.cs:64,83var url = LoginHelper.GetDefaultUrl(context, model.ReturnUrl, model.AppId, userService, dictService); ... return Redirect(url); // Redirect 不校验目标Utils/LoginHelper.cs:44-47if (appId == context.AppId) { return returnUrl ?? "/Admin/Index"; // 直接返回未校验的 returnUrl }- 首页组件同样:
Components/Pages/Home/Index.cs:84-85var url = LoginHelper.GetDefaultUrl(Context, ReturnUrl, AppId, UsersService, DictsService); NavigationManager.NavigateTo(url);Index.cs:27-36中ReturnUrl、AppId均为[SupplyParameterFromQuery](查询串可控)。context.AppId取配置值"BA"。
攻击路径: 构造链接 https://host/Home?AppId=BA&ReturnUrl=https://evil.com(或登录表单 POST AppId=BA&ReturnUrl=https://evil.com)→ 用户登录成功后浏览器被跳转到 https://evil.com → 钓鱼/诱导二次输入口令。
修复建议:
- 统一使用
LocalRedirect/Url.IsLocalUrl()校验,仅允许站内相对地址。 - 对
returnUrl增加协议与主机白名单(如仅允许http(s)且主机为本站/已配置前台域名)。
🟠 6. 未授权认证接口 + 无限制暴力破解
严重级别:中危 类别:认证缺陷 / 爆破 影响:用户账号
漏洞点:
Controllers/api/LoginController.cs 为 [AllowAnonymous],POST 返回明文认证结果:
[Route("api/[controller]")]
[AllowAnonymous]
[ApiController]
public class LoginController : ControllerBase
{
[HttpPost()]
public AuthenticateResult Post(LoginUser user, [FromQuery] bool mobile, ...)
{
...
result.Authenticated = userService.Authenticate(user.UserName, user.Password);
...
result.Error = "用户名或者密码错误"; // 错误信息可枚举
return result; // { Authenticated: true/false, Error: ... }
}
}
配合 UserService.Authenticate(UserService.cs:32-43)——无失败计数、无账号锁定、无速率限制;全项目搜索未发现任何 RateLimit/Throttle/Lockout 实现。
影响: 攻击者无需登录即可对任意用户名无限尝试密码(在线爆破),并通过"用户名或者密码错误"与"验证码不正确"的差异做账号枚举。配合 #3 弱哈希与 #2 默认口令,实际突破概率高。
修复建议:
- 登录接口增加服务端速率限制(IP/账号维度),如
AspNetCoreRateLimit或自研滑动窗口。 - 实现账号锁定策略(连续失败 N 次锁定,可配合验证码)。
- 统一错误信息,避免账号枚举;
Authenticated结果不要在未授权接口明文返回。
🟠 7. 短信验证码弱点 → 手机号账号接管
严重级别:中危 类别:认证缺陷 / 账号接管 影响:用户账号
漏洞点:
- 验证码仅 4 位且非加密安全随机:
TencentSMSProvider.cs:67var code = _random.Next(1000, 9999).ToString(); // 4 位、Random 非 CSPRNG(默认短信 Provider 为腾讯,
ServiceCollectionExtensions.cs:40) - 验证码校验无爆破防护、失败不失效:
TencentSMSProvider.cs:120public bool Validate(string phoneNumber, string code) => _pool.TryGetValue(phoneNumber, out var signKey) && code == signKey.Code;验证码存放于进程内
ConcurrentDictionary,无尝试次数限制、无失败后作废、无冷却。 - 发送验证码无服务端限流:
SMSLogin.razor.cs:29-59仅有客户端 60 秒倒计时(可被绕过),服务端SendCodeAsync不限制。 - 配合 #6 的未授权
api/Login?mobile=true认证接口,可直接爆破 4 位验证码。
攻击路径: 对目标手机号发送验证码(或诱导他人点击"发送验证码")→ 通过未授权接口对 0000~9999 爆破 → 校验通过后用手机号登录(LoginController.Mobile / TryCreateUserByPhone 会以该手机号创建/绑定账号并签发会话)→ 接管该手机号用户账号。
修复建议:
- 验证码长度提升至 6 位,用
RandomNumberGenerator(CSPRNG)生成。 - 服务端限制:同一手机号发送频率(如 1 条/60s、每日上限)、校验尝试次数(如 5 次失败后作废并要求重新发送)。
- 校验失败即删除/作废当前验证码。
🟠 8. SSRF(未授权,有限主机)
严重级别:中危 类别:SSRF / 开放代理滥用 影响:内网探测 / 数据外泄
漏洞点:
Controllers/api/GiteeController.cs 全部接口为 [AllowAnonymous],路径参数直接拼入服务端出站请求并回显响应:
[AllowAnonymous]
public class GiteeController : ControllerBase
{
// 用户可控 userName/repoName
var content = await client.HttpClient.GetStringAsync($"https://gitee.com/{userName}/{repoName}/issues");
...
var msg = regex.Select(...); // 解析结果回显给调用者
return new JsonResult(new { ..., message = msg, ... });
}
涉及接口:Issues、Pulls、Releases(https://gitee.com/{userName}/{repoName}/...)、Builds(https://ci.appveyor.com/api/projects/{userName}/{projName}/branch/{branchName})。
影响: 主机被限定在 gitee.com / ci.appveyor.com,但路径与分支名完全可控且结果回显——可被用来:探测/读取这两个站点任意公开路径资源、验证内网出网连通性、作为反射代理滥用。属于有限 SSRF / 开放代理。
修复建议:
- 对
userName/repoName/projName/branchName做白名单(本项目固定的LongbowEnterprise/BootstrapAdmin、ArgoZhang/bootstrapadmin等),禁止任意值。 - 接口加认证,禁止匿名调用。
- 出站请求统一走受控 HttpClient(固定超时、SSRF 防护:禁止跳转到非白名单主机、DNS 重绑定防护)。
🟡 9. XSS(MarkupString 渲染健康检查数据)
严重级别:低危 类别:XSS(存储型风险) 影响:用户
漏洞点:
Components/Components/HealthCheckDetails.razor.cs:33-38
private static MarkupString GetText(string? value)
{
var ret = value ?? "";
ret = ret.Replace("\n", "<br />");
return new MarkupString(ret); // 将数据原样作为 HTML 渲染
}
数据源为健康检查返回项 d.Value?.ToString()。当前健康检查项由服务端注册(DB/Gitee),非直接用户输入,风险较低;但该组件把任意值按 HTML 渲染,一旦任一健康检查数据混入用户可控内容(如 URL 回显、异常消息含用户输入),即触发 XSS。
修复建议: 健康检查详情用纯文本渲染(\n 换行用 CSS white-space: pre-wrap 或 @("\n".Replace...) 编码后拼接),避免 MarkupString 渲染未经转义的服务端数据。
🟡 10. 敏感信息外发 + 硬编码监控凭据
严重级别:低危 类别:信息泄露 / 硬编码 影响:数据保密性
漏洞点:
appsettings.json:32-34:Sentry DSNhttps://70bdfff562e84fa7b9a43d65924ab9ad@sentry.io/1469396appsettings.json:45-47:ExceptionlessApiKey: "AgQlY1MRWpX5qOF2edpK2IZYBhgPYImhr4UnZdAT"appsettings.json:15-20:Logging:Cloud:Url = "https://client.blazor.zone/api/Interface/Log"(云日志),HealthsCloudUrl同理- 启用云日志/健康上报后,错误日志(含堆栈、SQL、路径等敏感信息)会外发至第三方域名,且监控凭据硬编码在配置中。
修复建议:
- DSN/ApiKey/SecurityKey 一律改环境变量或密钥管理服务注入。
- 云日志开关默认关闭,并对日志内容做脱敏(隐藏口令、Token、连接串)。
🟡 11. CI/测试脚本硬编码数据库口令
严重级别:低危 类别:硬编码 影响:测试环境
漏洞点:
scripts/appveyor/appveyor.test.ps1:9,12
sqlcmd -S "$sqlInstance" -U sa -P Password12! -i "$sqlFile" -i "$initFile" ...
$env:MYSQL_PWD="Password12!"
SQL Server sa 与 MySQL 的测试口令 Password12! 硬编码在仓库中。虽为 CI 测试环境,但若测试库暴露或有复用口令风险,会造成横向影响。
修复建议: CI 口令使用 AppVeyor/GitHub Actions 的 Secret 注入;如已暴露,修改相应测试库口令。
🟡 12. 登录 CSRF(缺防伪令牌)
严重级别:低危 类别:CSRF 影响:用户会话
漏洞点:
Controllers/LoginController.cs:35-66 的 Login POST 无 [ValidateAntiForgeryToken](Blazor 全局 UseAntiforgery() 仅约束 Blazor 组件交互)。跨站可构造表单强制提交登录,把受害者登录进攻击者控制的账号(登录 CSRF)。
修复建议: 对登录 POST 添加 [ValidateAntiForgeryToken](同时为移动端/API 单独走带签名的接口)。
🟡 13. 商业许可证 Token 入库
严重级别:低危 类别:敏感文件泄露
src/blazor/admin/keys/Longbow.lic 为 BootstrapBlazor 商业授权文件,内含授权 token,随仓库提交公开。非认证凭据,但属商业授权资产,建议加入 .gitignore 并从历史中移除。
🟡 14. 访问控制依赖宽松子串匹配(设计层面)
严重级别:低危 类别:越权(水平/垂直风险)
BootstrapAdmin.Web.Core/Services/AdminService.cs:50-54
public Task<bool> AuthorizingNavigation(string userName, string url)
{
var ret = Navigations.GetAllMenus(userName).Any(m => m.Url.Contains(url, StringComparison.OrdinalIgnoreCase));
...
}
- 后台页面授权通过文件夹级
@attribute [Authorize](Components/Pages/Admin/_Imports.razor)仅校验"已登录";真正的权限由外部包Bootstrap.Security.Blazor依据AuthorizingNavigation做菜单 URL 匹配强制。 - 匹配使用子串
Contains+ 忽略大小写:若某菜单 URL 是另一 URL 的前缀/子串(如~/Admin/Index与/Admin/Index.html、或父级 URL),存在误授权可能;且页面数据层(DefaultDataService等)自身不校验角色,完全依赖该匹配。 - 授权判定强依赖外部包的导航拦截实现,绕过风险与框架版本强相关。
修复建议: 菜单 URL 匹配改为精确/规范 URL 匹配(归一化路径、大小写),并考虑在数据服务层增加角色鉴权兜底(防止"有页面无菜单也能直达数据")。
已排查、确认为非漏洞或风险可控项
| 项目 | 结论 |
|---|---|
| 反序列化 | 无 BinaryFormatter / TypeNameHandling / LosFormatter 等;唯一反序列化为 System.Text.Json 强类型(DefaultSMSProvider.cs:60、TencentSMSProvider.cs:87),安全 |
| 命令执行 / RCE | 无 Process.Start / Assembly.Load / 动态程序集加载 |
| SQL(主查询) | UserService/DictService/RoleService/GroupService/AppService/NavigationService 等全部使用 @0/@UserName 参数化;in (@roles) 由 PetaPoco 自动展开为参数(安全);n.{order} 为 EscapeSqlIdentifier("Order") 常量(非用户输入) |
| 未授权访问(页面) | 全部 15 个后台页面位于 Components/Pages/Admin/,有文件夹级 [Authorize](见 #14 的设计缺陷说明) |
Admin/Sql 页面 |
仅为空壳 <h3>SQL</h3>,无 SQL 控制台功能 |
AccountController |
仅 AccessDenied 返回 Ok(),无敏感操作 |
| 反代/加密传输 | Cookie 认证依赖框架默认(Cookie.SecurePolicy 未显式配置,建议 HTTPS 环境下显式设置 SameSite/Secure/HttpOnly) |
模拟登录 SimulateUserName |
仅存在于客户端模板 appsettings.Development.json,且为开发环境模拟功能 |
修复优先级建议
- 立即(高危):#1 排序 SQL 注入、#2 默认口令、#4 弱 JWT 密钥 + 吊销泄露 OAuth 密钥。
- 尽快(中危):#6 登录接口限流/锁定、#7 验证码加固、#5 开放重定向、#8 SSRF 白名单。
- 规划(低危):#3 密码哈希升级、#9~#13 加固、#14 ACL 精确化。
建议按上述顺序完成修复后,重新对本仓库做一次回归审计,重点验证排序字段白名单与登录爆破防护是否到位。