首页
/ ECC 项目 Kotlin 安全规则解析:从密钥管理、SQL 注入防护到 Ktor JWT 认证的 Cursor 安全规范

ECC 项目 Kotlin 安全规则解析:从密钥管理、SQL 注入防护到 Ktor JWT 认证的 Cursor 安全规范

2026-09-06 11:31:31作者:平淮齐Percy

本篇指南基于 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 提供了这条规则在生产代码中的完整落地形态,可以视为该安全规则的参考实现:

  1. 所有查询包裹在 newSuspendedTransaction,既保证协程安全,也让 DSL 查询天然处于参数化框架内:

    suspend fun findByEmail(email: String): User? =
        newSuspendedTransaction(db = database) {
            UsersTable.selectAll()
                .where { UsersTable.email eq email }
                .map { it.toUser() }
                .singleOrNull()
        }
    
  2. LIKE 模糊搜索有专门的通配符转义。即使使用了参数化查询,用户输入中的 %_ 通配符仍会被 LIKE 语义解释,规则文档补充了防护:

    // LIKE and pattern matching — always escape user input to prevent wildcard injection
    private fun escapeLikePattern(input: String): String =
        input.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
    

    这提示我们:参数化只解决“注入”,不解决“语义注入”(通配符滥用导致的暴力枚举或性能攻击),两者需要分别处理。

  3. 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
            }
        }
    }
}

这段配置里有几个关键安全决策值得逐条拆解:

  • 命名 providerjwt("jwt") 中的 "jwt" 是 provider 名,路由层用 authenticate("jwt") { ... } 精确引用它,支持未来并存多种认证方式(如基础认证 + JWT);
  • verifier 强制算法JWT.require(Algorithm.HMAC256(secret)) 指定验签算法,避免“none”算法或算法混淆攻击;secret 本身必须来自上文的环境变量/配置占位,而不是硬编码;
  • withAudience / withIssuer 在验签器层预校验:token 的 audiss 声明不符时,require() 构建的 verifier 会在验签阶段直接拒绝;
  • validate 二次防御:即使 verifier 已预校验,validate 块仍独立复核 payload.audience.contains(audience)payload.issuer == issuerpayload.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 安全规则时可按以下清单自查:

  1. 所有密钥(API key、JWT secret、数据库口令)来自环境变量或配置占位符,启动时用 ?: throw 模式做存在性校验;
  2. 数据库访问 100% 走 Exposed DSL(where { column eq value })或 DAO,禁止 exec 拼接用户输入;LIKE 查询额外转义 %_\
  3. Ktor 认证使用命名 jwt provider,JWT.require 固定算法并预校验 aud/issvalidate 块二次复核,challenge 统一 401 响应;
  4. 受保护路由一律包在 authenticate("jwt") { } 内,并用 testApplication 验证“无凭据 401 / 有效凭据放行”两条路径;
  5. 代码审查时将 !! 视为安全反模式,优先用 ?:?.let、密封类表达空分支。

这套规则的完整原文见 .cursor/rules/kotlin-security.md,配套的完整 Ktor/Exposed 工程模板与测试用例分别在 skills/kotlin-ktor-patterns/SKILL.mdskills/kotlin-exposed-patterns/SKILL.md,可直接对照仓库阅读并复用。

登录后查看全文
热门项目推荐
相关项目推荐