安全最佳实践:SQL注入、输入校验与凭据管理
本教程共 54 篇 · 第 44 篇 · 更新于 2026-08-11 · 约 5 分钟阅读
本节目标:掌握 Prisma 应用的六道安全防线——注入防护、输入校验、字段脱敏、凭据管理、行级安全与审计限流。
ORM 挡不住所有安全问题。Prisma 的类型安全解决「写错」,解决不了「被攻击」。本章把生产环境必须做的六件事讲清楚。
SQL 注入:模板字符串就是盾牌
Prisma Client 的常规 API 天然防注入:参数走预编译,不拼进 SQL。风险集中在原始查询(Raw Query)。$queryRaw 的标签模板会自动把 ${} 里的值参数化:
const email = "a@example.com";
const users = await prisma.$queryRaw`
SELECT * FROM "User" WHERE email = ${email}
`;
条件要动态组合时,用 Prisma.sql 拼接。它同样参数化,可以安全组合:
const where = Prisma.sql`WHERE role = ${role} AND "deletedAt" IS NULL`;
await prisma.$queryRaw`SELECT * FROM "User" ${where}`;
$queryRawUnsafe 接收普通字符串,是唯一可能注入的位置。底线:用户输入绝不直接拼进去。必须用时,只接白名单,比如排序字段映射:
const SORT_COLUMNS = ["createdAt", "updatedAt"] as const;
// sortBy 来自用户,先查白名单,查不到就用默认值
const column = SORT_COLUMNS.includes(sortBy) ? sortBy : "createdAt";
await prisma.$queryRawUnsafe(`SELECT * FROM "Post" ORDER BY "${column}" DESC`);
Warning字符串拼接
SELECT ... WHERE email = '${email}'是最经典的注入写法,一次都不要出现。
输入校验:编译期类型不等于运行时安全
TypeScript 类型在编译后消失。z.string() 只存在于编辑器里,运行时拦不住任何恶意请求。Zod 等校验库在运行时重新检查:
import { z } from "zod";
const createUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
});
async function createUser(input: unknown) {
const data = createUserSchema.parse(input);
return prisma.user.create({ data });
}
入口处 parse 一次,后续代码拿到的 data 才是可信的。校验通过后类型自动推导,直接喂给 create。
字段级脱敏:敏感字段不出门
密码、token 不该出现在查询结果里。两种做法。一是全局 omit:实例化时配置,所有查询默认剔除:
import { PrismaClient } from "./generated/prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
const prisma = new PrismaClient({
adapter,
omit: { user: { password: true } },
});
二是用 result 组件做打码展示。要返回掩码版本而不泄露原文:
const safePrisma = prisma.$extends({
result: {
user: {
maskedEmail: {
needs: { email: true },
compute(user) {
return user.email.replace(/^(.).+(@.*)$/, "$1***$2");
},
},
},
},
});
注意:omit 配置在构造函数里,对全部查询生效;result 组件挂到扩展客户端上,只对使用它的代码生效。需要完整结果但绝不能带密码的内部场景,就绕开扩展客户端。
Tipomit 适合「彻底不给」,result 组件适合「给了但不能看全」。两层可以叠加,机制细节见第 36 章。
凭据管理:.env 不进门
数据库连接字符串等于数据库的钥匙。三条纪律:
.env永远进 .gitignore,仓库里只放.env.example模板。- 生产凭据放密钥管理服务(AWS Secrets Manager、Vault、Doppler),部署时注入环境变量,不进镜像、不进日志。
- 定期轮换,出事立刻轮换。
应用启动时用 Zod 校验环境变量,缺了早报错,别等第一次查询才炸:
const envSchema = z.object({
DATABASE_URL: z.string().url(),
NODE_ENV: z.enum(["development", "test", "production"]),
});
const env = envSchema.parse(process.env);
数据库账号同样要最小权限。运行账号和迁移账号分开:应用只拿 SELECT/INSERT/UPDATE/DELETE 权限,迁移才需要 DDL。运行时绝不使用超级用户。
行级安全:数据库把好最后一关
多租户应用最怕「用户 A 查到用户 B 的数据」。应用层过滤漏一个 where,数据就泄露。PostgreSQL 行级安全(Row-Level Security,RLS)让数据库强制隔离,绕过应用也读不到:
ALTER TABLE "Post" ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON "Post"
USING ("tenantId" = current_setting('app.tenant_id', true)::int);
事务开头用 $executeRaw 设置上下文,事务内所有查询自动带上租户条件:
await prisma.$transaction(async (tx) => {
await tx.$executeRaw`SELECT set_config('app.tenant_id', ${tenantId}, true)`;
// 后续查询只能看到本租户的数据
});
不想上 RLS 的团队,退而求其次:用 $extends 的 query 组件全局注入 tenantId 过滤。它防不住应用层 bug,但比手写 where 可靠得多。
审计日志与速率限制
审计回答「谁改了什么」。第 36 章的 $allOperations 扩展可以直接用于安全合规:写操作落审计表,恢复等敏感操作单独记录。生产建议:审计表与业务表分开存储,用追加型(append-only)存储防篡改。
速率限制防暴力破解与刷接口。三层:API 网关(Cloudflare、NGINX)挡在最前;应用层用 express-rate-limit 加 Redis 计数;数据库层设语句超时与连接数上限,防慢查询拖垮实例。
参考来源
- Prisma 官方文档:Best practices(SQL injection / sensitive data / input validation)
- Mapagam:Implementing Security Best Practices
- Tech Insider:Prisma ORM Tutorial 2026(pitfalls 与 troubleshooting 章节)