BootstrapAdmin-安全审计报告

12次阅读
没有评论

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:77
    var 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:59
    sortList.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:66
    sql.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 做白名单校验,直接透传。

修复建议:

  1. 在数据服务层对 SortName 做白名单校验:仅允许 TModel 的真实属性名(用反射或 [Sortable] 属性白名单),否则回退默认排序。
  2. 对 sortList 中的每一项同样做属性名白名单过滤后再 OrderBy。
  3. 统一封装一个 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 角色),进而管理全部用户、角色、菜单、字典。

修复建议:

  1. 移除种子脚本中的固定口令;首次启动强制修改初始密码(或生成随机密码)。
  2. 生产环境删除演示账号 User;关闭 DEBUG 预填。
  3. 对密码复杂度、首次登录改密做强约束。

🔴 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 每秒数十亿次),普通用户口令几乎必破。

修复建议:

  1. 改用 Rfc2898DeriveBytes(PBKDF2-SHA256,迭代次数 ≥ 100k)或 bcrypt/Argon2id。
  2. 新口令入库时用新算法,老口令校验时逐步迁移(登录成功即重哈希)。

🔴 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 密钥泄露可滥用第三方账号授权。

修复建议:

  1. SecurityKey 改用强随机密钥并从环境变量/密钥管理服务(如用户机密、KMS)注入,禁止入库。
  2. 立即在 Gitee/GitHub 后台吊销并重置当前泄露的 ClientSecret,改为环境变量注入。
  3. 全仓库(含历史提交)清除 appsettings.Development.json 等含真实密钥的文件;.gitignore 中忽略本地密钥文件。

🟠 5. 开放重定向(Open Redirect)

严重级别:中危 类别:钓鱼/诱导 影响:用户

漏洞点:

登录成功后的跳转目标由用户可控参数直接决定,未校验是否为站内地址:

  • Controllers/LoginController.cs:64,83
    var url = LoginHelper.GetDefaultUrl(context, model.ReturnUrl, model.AppId, userService, dictService);
    ...
    return Redirect(url);   // Redirect 不校验目标
    
  • Utils/LoginHelper.cs:44-47
    if (appId == context.AppId)
    {
        return returnUrl ?? "/Admin/Index";   // 直接返回未校验的 returnUrl
    }
    
  • 首页组件同样:Components/Pages/Home/Index.cs:84-85
    var 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 → 钓鱼/诱导二次输入口令。

修复建议:

  1. 统一使用 LocalRedirect / Url.IsLocalUrl() 校验,仅允许站内相对地址。
  2. 对 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 默认口令,实际突破概率高。

修复建议:

  1. 登录接口增加服务端速率限制(IP/账号维度),如 AspNetCoreRateLimit 或自研滑动窗口。
  2. 实现账号锁定策略(连续失败 N 次锁定,可配合验证码)。
  3. 统一错误信息,避免账号枚举;Authenticated 结果不要在未授权接口明文返回。

🟠 7. 短信验证码弱点 → 手机号账号接管

严重级别:中危 类别:认证缺陷 / 账号接管 影响:用户账号

漏洞点:

  • 验证码仅 4 位且非加密安全随机: TencentSMSProvider.cs:67
    var code = _random.Next(1000, 9999).ToString();   // 4 位、Random 非 CSPRNG
    

    (默认短信 Provider 为腾讯,ServiceCollectionExtensions.cs:40)

  • 验证码校验无爆破防护、失败不失效: TencentSMSProvider.cs:120
    public 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 会以该手机号创建/绑定账号并签发会话)→ 接管该手机号用户账号。

修复建议:

  1. 验证码长度提升至 6 位,用 RandomNumberGenerator(CSPRNG)生成。
  2. 服务端限制:同一手机号发送频率(如 1 条/60s、每日上限)、校验尝试次数(如 5 次失败后作废并要求重新发送)。
  3. 校验失败即删除/作废当前验证码。

🟠 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 / 开放代理。

修复建议:

  1. 对 userName/repoName/projName/branchName 做白名单(本项目固定的 LongbowEnterprise/BootstrapAdmin、ArgoZhang/bootstrapadmin 等),禁止任意值。
  2. 接口加认证,禁止匿名调用。
  3. 出站请求统一走受控 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 DSN https://70bdfff562e84fa7b9a43d65924ab9ad@sentry.io/1469396
  • appsettings.json:45-47:Exceptionless ApiKey: "AgQlY1MRWpX5qOF2edpK2IZYBhgPYImhr4UnZdAT"
  • appsettings.json:15-20:Logging:Cloud:Url = "https://client.blazor.zone/api/Interface/Log"(云日志),HealthsCloudUrl 同理
  • 启用云日志/健康上报后,错误日志(含堆栈、SQL、路径等敏感信息)会外发至第三方域名,且监控凭据硬编码在配置中。

修复建议:

  1. DSN/ApiKey/SecurityKey 一律改环境变量或密钥管理服务注入。
  2. 云日志开关默认关闭,并对日志内容做脱敏(隐藏口令、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. 立即(高危):#1 排序 SQL 注入、#2 默认口令、#4 弱 JWT 密钥 + 吊销泄露 OAuth 密钥。
  2. 尽快(中危):#6 登录接口限流/锁定、#7 验证码加固、#5 开放重定向、#8 SSRF 白名单。
  3. 规划(低危):#3 密码哈希升级、#9~#13 加固、#14 ACL 精确化。

建议按上述顺序完成修复后,重新对本仓库做一次回归审计,重点验证排序字段白名单与登录爆破防护是否到位。

正文完
 0