ECC 项目 Kotlin 安全规则解析:从密钥管理、SQL 注入防护到 Ktor JWT 认证的 Cursor 安全规范
本篇指南基于 ECC 仓库中的 Cursor 规则文件 kotlin-security.md 展开,系统讲解该规则如何为 Kotlin/Gradle 工程强制注入安全约束:环境变量读取密钥、Exposed 参数化查询防 SQL 注入、Ktor Auth 插件实现 JWT 认证,以及把 Kotlin 空安全机制转化为安全防线。读完本文,你将理解这类“规则即代码”(Rule-as-Code)文件在 ECC 体系中的装配机制,并掌握可直接复制运行的安全编码模板。
一、规则文件的定位与装配机制
kotlin-security.md 位于 ECC 仓库的 .cursor/rules/ 目录,是一个带 YAML frontmatter 的 Cursor 项目规则文件。其头部元数据决定了它在 Agent 工作流中的激活方式:
---
description: "Kotlin security extending common rules"
globs: ["**/*.kt", "**/*.kts", "**/build.gradle.kts"]
alwaysApply: false
---
三个字段的作用:
globs:规则只对匹配**/*.kt、**/*.kts、**/build.gradle.kts的文件生效。也就是说,只要 Agent 正在编辑 Kotlin 源码、Kotlin DSL 脚本或 Gradle 构建脚本,这条安全规则就会被自动附带进上下文;alwaysApply: false:非全局常驻规则。与之形成对照的是同目录的 common-security.md,其 frontmatter 中alwaysApply: true,属于每次会话都生效的通用安全基线(提交前检查清单、密钥管理禁令、安全事件响应协议)。kotlin-security 的规则定位正是文档开头声明的“extends the common security rule”——在通用基线之上叠加 Kotlin 生态的具体实践;description:向 Agent 说明该规则的职责边界,便于在多规则并存时被正确引用。
从源码结构看,.cursor/rules/ 并非仅供 ECC 仓库自身使用。安装模块 cursor-project.js 中定义了 sourceRelativePath: '.cursor/rules' 的扁平化规则安装逻辑(createFlatRuleOperations),会把这些规则文件按统一命名转换后安装到目标项目的 rules 目录;同步脚本 sync-ecc-to-codex.sh 也显式引用了 CURSOR_RULES_DIR="$REPO_ROOT/.cursor/rules"。可以推断,该规则文件是 ECC 分发体系的一部分:用户在任意 Kotlin 项目中安装 ECC 后,Agent 即可按 globs 自动获得这套安全约束。
二、Secret Management:环境变量读取密钥并启动即校验
规则给出的核心模板只有三行,但它同时落实了通用安全规则(common-security.md 中“NEVER hardcode secrets”“Validate that required secrets are present at startup”)的三条要求:
val apiKey = System.getenv("API_KEY")
?: throw IllegalStateException("API_KEY not configured")
逐点解读:
System.getenv("API_KEY"):密钥唯一来源是运行环境,源码中不出现任何字面量密钥;- Elvis 运算符
?:配合throw IllegalStateException:这是“启动即校验”(fail-fast)模式。缺失密钥时进程直接抛出可识别的异常,而不是带着null密钥运行到某个 API 调用点才暴露问题; - 异常消息仅说明配置名,不泄露任何密钥内容。
这套写法与 ECC 的 Ktor 技能文档 kotlin-ktor-patterns/SKILL.md 中的配置实践一致:该文档演示了 environment.config.property("jwt.secret").getString() 这类从 application.yaml 读取配置、再由 ${JWT_SECRET} 环境变量占位注入的做法,本质上是同一原则在框架层的延伸——敏感值永远来自外部环境,配置文件里只留占位符。
对于 Android/KMP 场景,仓库中的完整规则 rules/kotlin/security.md 补充了更细的密钥分级策略:本地开发用 git-ignored 的 local.properties,发布构建用 CI 生成的 BuildConfig 字段,运行时敏感值用 EncryptedSharedPreferences(Android)或 Keychain(iOS)。.cursor 版本的三行模板是服务端(JVM)场景的最小实现,而 BuildConfig / 安全存储则是同一原则在移动端的应用。
三、SQL Injection Prevention:Exposed DSL 参数化查询
规则强制要求所有数据库访问走 Exposed 的参数化 DSL,并给出正反对照:
// Good: Parameterized via Exposed DSL
UsersTable.selectAll().where { UsersTable.email eq email }
// Bad: String interpolation in raw SQL
exec("SELECT * FROM users WHERE email = '$email'")
两种写法的本质差异在于:where { UsersTable.email eq email } 由 Exposed 生成带占位符的 PreparedStatement,email 作为绑定参数传递,数据库永远把它当作数据而非 SQL 片段;而字符串插值版本直接把用户输入拼进 SQL 文本,攻击者构造 email = "' OR '1'='1" 即可改写语句语义。
ECC 仓库的 Exposed 技能文档 kotlin-exposed-patterns/SKILL.md 提供了这条规则在生产代码中的完整落地形态,可以视为该安全规则的参考实现:
-
所有查询包裹在
newSuspendedTransaction中,既保证协程安全,也让 DSL 查询天然处于参数化框架内:suspend fun findByEmail(email: String): User? = newSuspendedTransaction(db = database) { UsersTable.selectAll() .where { UsersTable.email eq email } .map { it.toUser() } .singleOrNull() } -
LIKE 模糊搜索有专门的通配符转义。即使使用了参数化查询,用户输入中的
%、_通配符仍会被 LIKE 语义解释,规则文档补充了防护:// LIKE and pattern matching — always escape user input to prevent wildcard injection private fun escapeLikePattern(input: String): String = input.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")这提示我们:参数化只解决“注入”,不解决“语义注入”(通配符滥用导致的暴力枚举或性能攻击),两者需要分别处理。
-
Repository 模式隔离数据访问。技能文档把上述查询封装在
UserRepository接口之后,业务层接触不到任何 SQL 拼接点,进一步压缩了误用exec原始 SQL 的空间。
同属 Kotlin 生态的 Room/SQLDelight 场景,rules/kotlin/security.md 给出了对应的输入校验规则:@Query 中禁止 '$input' 插值,必须使用 :input 命名参数——与 Exposed DSL 规则在原理上完全同构。
四、Authentication:Ktor Auth 插件 + JWT 验签
规则给出的认证方案是 Ktor 的 Authentication 插件配合 JWT,完整配置如下:
install(Authentication) {
jwt("jwt") {
verifier(
JWT.require(Algorithm.HMAC256(secret))
.withAudience(audience)
.withIssuer(issuer)
.build()
)
validate { credential ->
val payload = credential.payload
if (payload.audience.contains(audience) &&
payload.issuer == issuer &&
payload.subject != null) {
JWTPrincipal(payload)
} else {
null
}
}
}
}
这段配置里有几个关键安全决策值得逐条拆解:
- 命名 provider:
jwt("jwt")中的"jwt"是 provider 名,路由层用authenticate("jwt") { ... }精确引用它,支持未来并存多种认证方式(如基础认证 + JWT); verifier强制算法:JWT.require(Algorithm.HMAC256(secret))指定验签算法,避免“none”算法或算法混淆攻击;secret本身必须来自上文的环境变量/配置占位,而不是硬编码;withAudience/withIssuer在验签器层预校验:token 的aud和iss声明不符时,require()构建的 verifier 会在验签阶段直接拒绝;validate二次防御:即使 verifier 已预校验,validate块仍独立复核payload.audience.contains(audience)、payload.issuer == issuer和payload.subject != null,任何一项不满足返回null,Ktor 随即走 401 challenge 流程。这种“双层校验”防止 verifier 配置遗漏时出现未认证放行。
ECC 的 Ktor 技能文档 skills/kotlin-ktor-patterns/SKILL.md 展示了该规则在真实工程中的完整形态,补充了规则模板未涉及的三个环节:
fun Application.configureAuthentication() {
val jwtSecret = environment.config.property("jwt.secret").getString()
val jwtIssuer = environment.config.property("jwt.issuer").getString()
val jwtAudience = environment.config.property("jwt.audience").getString()
val jwtRealm = environment.config.property("jwt.realm").getString()
install(Authentication) {
jwt("jwt") {
realm = jwtRealm
verifier(
JWT.require(Algorithm.HMAC256(jwtSecret))
.withAudience(jwtAudience)
.withIssuer(jwtIssuer)
.build()
)
validate { credential ->
if (credential.payload.audience.contains(jwtAudience)) {
JWTPrincipal(credential.payload)
} else {
null
}
}
challenge { _, _ ->
call.respond(HttpStatusCode.Unauthorized, ApiResponse.error<Unit>("Invalid or expired token"))
}
}
}
}
- 所有
jwt.*配置项从 Ktor 配置读取,对应application.yaml中的${JWT_SECRET}环境变量占位,把第二节“密钥不进源码”的原则贯彻到认证模块; challenge块统一了 401 响应体,避免各路由自行拼接错误消息而泄露内部细节;- 认证状态通过
principal<JWTPrincipal>()提取,业务代码中不手动解析Authorization头。
路由层的保护写法同样是“规则生效”的关键一环:
route("/users") {
get { /* 公开列表 */ }
// Protected routes
authenticate("jwt") {
post { /* 需要认证 */ }
put("/{id}") { /* 需要认证 */ }
delete("/{id}") { /* 需要认证 */ }
}
}
并且技能文档给出了对应的集成测试(testApplication + bearerAuth(token)),验证“无 token 返回 401、有效 token 返回 201”,这套测试用例可以直接作为上述认证配置的验收标准。
五、Null Safety as Security:把空安全当安全机制使用
规则最后一条只有一句话,但指向 Kotlin 类型系统的一个深层安全属性:
Kotlin's type system prevents null-related vulnerabilities -- avoid
!!to maintain this guarantee.
Kotlin 的不可空类型(T)在编译期保证变量不为 null,从而在语言层面消除了一类在 Java/C 系语言中常见的漏洞模式:空指针导致的未定义行为、绕过空检查的崩溃路径、以及因强制解包而跳过校验的分支。!! 强制解包运算符(value!!)会把这个保证撕开一个口子——它让 T? 静默变成 T,一旦运行时值为 null,就抛出不可控的 KotlinNullPointerException,或更糟地让上游“已判空”的假设在编译期失效。
从该规则的上下文看,它与通用安全基线(common-security.md 中“All user inputs validated”“Error messages don't leak sensitive data”)呼应:把不可空类型当作数据契约的一部分,用户输入在进入业务逻辑前以可空类型显式建模、显式处理(如第二节中的 ?: throw),而不是用 !! 掩盖。配合同目录的 kotlin-patterns.md 中推崇的密封类建模(Result<T> 显式表达成功/失败分支),Kotlin 项目可以用类型系统把“未认证”“参数缺失”“数据为空”等异常状态从运行时错误提升为编译期必须处理的分支。
六、与通用安全基线的关系
.cursor/rules/kotlin-security.md 不是孤立存在的。理解它与 common-security.md 的分工,才能完整使用这套规则:
| 维度 | common-security(alwaysApply: true) | kotlin-security(globs 触发) |
|---|---|---|
| 生效范围 | 所有文件、所有会话 | 仅 .kt / .kts / build.gradle.kts |
| 密钥管理 | 原则层:禁用硬编码、启动校验、暴露即轮换 | 实现层:System.getenv + fail-fast 模板 |
| SQL 注入 | 检查项:“parameterized queries” | 实现层:Exposed DSL 正反示例 |
| 认证 | 检查项:“authentication verified” | 实现层:Ktor JWT 完整配置 |
| 响应协议 | 发现安全问题:STOP、调用 security-reviewer agent、先修 CRITICAL | 不提供 |
也就是说,通用规则定义“必须检查什么”,Kotlin 规则定义“在 Kotlin 代码里怎么做”。Agent 在编辑 Kotlin 文件时同时携带两份上下文:rules/kotlin/security.md 中更完整的移动端补充(network_security_config.xml 禁止明文流量、OkHttp/Ktor 证书固定、@Query 参数化、ProGuard keep 规则等)可视为该体系在 Android 方向的延伸,而 .cursor 版本聚焦服务端 JVM 场景。
七、实践清单
结合本文内容,在 Kotlin/Ktor 项目落地这套 ECC 安全规则时可按以下清单自查:
- 所有密钥(API key、JWT secret、数据库口令)来自环境变量或配置占位符,启动时用
?: throw模式做存在性校验; - 数据库访问 100% 走 Exposed DSL(
where { column eq value })或 DAO,禁止exec拼接用户输入;LIKE 查询额外转义%、_、\; - Ktor 认证使用命名
jwtprovider,JWT.require固定算法并预校验aud/iss,validate块二次复核,challenge统一 401 响应; - 受保护路由一律包在
authenticate("jwt") { }内,并用testApplication验证“无凭据 401 / 有效凭据放行”两条路径; - 代码审查时将
!!视为安全反模式,优先用?:、?.let、密封类表达空分支。
这套规则的完整原文见 .cursor/rules/kotlin-security.md,配套的完整 Ktor/Exposed 工程模板与测试用例分别在 skills/kotlin-ktor-patterns/SKILL.md 和 skills/kotlin-exposed-patterns/SKILL.md,可直接对照仓库阅读并复用。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00