数据安全
¥Data Security
React 服务器组件 提升了性能并简化了数据获取,同时也改变了数据访问的位置和方式,从而改变了前端应用中处理数据的一些传统安全假设。
¥React Server Components improve performance and simplify data fetching, but also shift where and how data is accessed, changing some of the traditional security assumptions for handling data in frontend apps.
本指南将帮助你了解如何考虑 Next.js 中的数据安全性以及如何实现最佳实践。
¥This guide will help you understand how to think about data security in Next.js and how to implement best practices.
数据获取方法
¥Data fetching approaches
根据项目的规模和使用年限,我们推荐在 Next.js 中获取数据的三种主要方法:
¥There are three main approaches we recommend for fetching data in Next.js, depending on the size and age of your project:
HTTP API:适用于现有的大型应用和组织。
¥HTTP APIs: for existing large applications and organizations.
数据访问层:用于新项目。
¥Data Access Layer: for new projects.
组件级数据访问:用于原型设计和学习。
¥Component-Level Data Access: for prototypes and learning.
我们建议选择一种数据获取方法,并避免混合使用。这让使用你代码库的开发者和安全审计人员都能清楚地了解预期结果。
¥We recommend choosing one data fetching approach and avoiding mixing them. This makes it clear for both developers working in your code base and security auditors what to expect.
外部 HTTP API
¥External HTTP APIs
在现有项目中采用服务器组件时,你应该遵循零信任模型。你可以继续使用 fetch
从服务器组件调用现有的 API 端点(例如 REST 或 GraphQL),就像在客户端组件中一样。
¥You should follow a Zero Trust model when adopting Server Components in an existing project. You can continue calling your existing API endpoints such as REST or GraphQL from Server Components using fetch
, just as you would in Client Components.
import { cookies } from 'next/headers'
export default async function Page() {
const cookieStore = cookies()
const token = cookieStore.get('AUTH_TOKEN')?.value
const res = await fetch('https://api.example.com/profile', {
headers: {
Cookie: `AUTH_TOKEN=${token}`,
// Other headers
},
})
// ....
}
这种方法在以下情况下效果很好:
¥This approach works well when:
你已经实现了安全措施。
¥You already have security practices in place.
独立的后端团队使用其他语言或独立管理 API。
¥Separate backend teams use other languages or manage APIs independently.
数据访问层
¥Data Access Layer
对于新项目,我们建议创建专用的数据访问层 (DAL)。这是一个内部库,用于控制如何以及何时获取数据,以及将哪些内容传递给渲染上下文。
¥For new projects, we recommend creating a dedicated Data Access Layer (DAL). This is a internal library that controls how and when data is fetched, and what gets passed to your render context.
数据访问层应该:
¥A Data Access Layer should:
仅在服务器上运行。
¥Only run on the server.
执行授权检查。
¥Perform authorization checks.
返回安全、最小化的数据传输对象 (DTO)。
¥Return safe, minimal Data Transfer Objects (DTOs).
这种方法集中了所有数据访问逻辑,使强制执行一致的数据访问变得更加容易,并降低了授权错误的风险。你还可以享受在请求的不同部分共享内存缓存的好处。
¥This approach centralizes all data access logic, making it easier to enforce consistent data access and reduces the risk of authorization bugs. You also get the benefit of sharing an in-memory cache across different parts of a request.
import { cache } from 'react'
import { cookies } from 'next/headers'
// Cached helper methods makes it easy to get the same value in many places
// without manually passing it around. This discourages passing it from Server
// Component to Server Component which minimizes risk of passing it to a Client
// Component.
export const getCurrentUser = cache(async () => {
const token = cookies().get('AUTH_TOKEN')
const decodedToken = await decryptAndValidate(token)
// Don't include secret tokens or private information as public fields.
// Use classes to avoid accidentally passing the whole object to the client.
return new User(decodedToken.id)
})
import 'server-only'
import { getCurrentUser } from './auth'
function canSeeUsername(viewer: User) {
// Public info for now, but can change
return true
}
function canSeePhoneNumber(viewer: User, team: string) {
// Privacy rules
return viewer.isAdmin || team === viewer.team
}
export async function getProfileDTO(slug: string) {
// Don't pass values, read back cached values, also solves context and easier to make it lazy
// use a database API that supports safe templating of queries
const [rows] = await sql`SELECT * FROM user WHERE slug = ${slug}`
const userData = rows[0]
const currentUser = await getCurrentUser()
// only return the data relevant for this query and not everything
// <https://www.w3.org/2001/tag/doc/APIMinimization>
return {
username: canSeeUsername(currentUser) ? userData.username : null,
phonenumber: canSeePhoneNumber(currentUser, userData.team)
? userData.phonenumber
: null,
}
}
import { getProfile } from '../../data/user'
export async function Page({ params: { slug } }) {
// This page can now safely pass around this profile knowing
// that it shouldn't contain anything sensitive.
const profile = await getProfile(slug);
...
}
需要了解:密钥应存储在环境变量中,但只有数据访问层才能访问
process.env
。这可以防止机密信息暴露给应用的其他部分。¥Good to know: Secret keys should be stored in environment variables, but only the Data Access Layer should access
process.env
. This keeps secrets from being exposed to other parts of the application.
组件级数据访问
¥Component-level data access
为了快速原型和迭代,数据库查询可以直接放在服务器组件中。
¥For quick prototypes and iteration, database queries can be placed directly in Server Components.
但是,这种方法更容易意外地将私有数据暴露给客户端,例如:
¥This approach, however, makes it easier to accidentally expose private data to the client, for example:
import Profile from './components/profile.tsx'
export async function Page({ params: { slug } }) {
const [rows] = await sql`SELECT * FROM user WHERE slug = ${slug}`
const userData = rows[0]
// EXPOSED: This exposes all the fields in userData to the client because
// we are passing the data from the Server Component to the Client.
return <Profile user={userData} />
}
'use client'
// BAD: This is a bad props interface because it accepts way more data than the
// Client Component needs and it encourages server components to pass all that
// data down. A better solution would be to accept a limited object with just
// the fields necessary for rendering the profile.
export default async function Profile({ user }: { user: User }) {
return (
<div>
<h1>{user.name}</h1>
...
</div>
)
}
你应该在将数据传递给客户端组件之前对其进行清理:
¥You should sanitize the data before passing it to the Client Component:
import { sql } from './db'
export async function getUser(slug: string) {
const [rows] = await sql`SELECT * FROM user WHERE slug = ${slug}`
const user = rows[0]
// Return only the public fields
return {
name: user.name,
}
}
import { getUser } from '../data/user'
import Profile from './ui/profile'
export default async function Page({
params: { slug },
}: {
params: { slug: string }
}) {
const publicProfile = await getUser(slug)
return <Profile user={publicProfile} />
}
读取数据
¥Reading data
将数据从服务器传递到客户端
¥Passing data from server to client
在初始加载时,服务器和客户端组件都在服务器上运行以生成 HTML。但是,它们在独立的模块系统中执行。这确保服务器组件可以访问私有数据和 API,而客户端组件则不能。
¥On the initial load, both Server and Client Components run on the server to generate HTML. However, they execute in isolated module systems. This ensures that Server Components can access private data and APIs, while Client Components cannot.
服务器组件:
¥Server Components:
仅在服务器上运行。
¥Run only on the server.
可以安全地访问环境变量、机密信息、数据库和内部 API。
¥Can safely access environment variables, secrets, databases, and internal APIs.
客户端组件:
¥Client Components:
在预渲染期间在服务器上运行,但必须遵循与浏览器中运行的代码相同的安全假设。
¥Run on the server during pre-rendering, but must follow the same security assumptions as code running in the browser.
不得访问特权数据或仅限服务器的模块。
¥Must not access privileged data or server-only modules.
这确保应用默认是安全的,但数据获取或传递给组件的方式可能会意外泄露私有数据。
¥This ensures the app is secure by default, but it's possible to accidentally expose private data through how data is fetched or passed to components.
污染
¥Tainting
为防止隐私数据意外泄露给客户端,你可以使用 React Taint API:
¥To prevent accidental exposure of private data to the client, you can use React Taint APIs:
experimental_taintObjectReference
用于数据对象。¥
experimental_taintObjectReference
for data objects.experimental_taintUniqueValue
用于特定值。¥
experimental_taintUniqueValue
for specific values.
你可以在 Next.js 应用中使用 next.config.js
中的 experimental.taint
选项启用预取:
¥You can enable usage in your Next.js app with the experimental.taint
option in next.config.js
:
这可以防止受污染的对象或值传递给客户端。但是,这是额外的保护层,在将 DAL 中的数据传递给 React 的渲染上下文之前,你仍然应该对其进行过滤和清理。
¥This prevents the tainted objects or values from being passed to the client. However, it's an additional layer of protection, you should still filter and sanitize the data in your DAL before passing it to React's render context.
需要了解:
¥Good to know:
默认情况下,环境变量仅在服务器上可用。Next.js 会将所有以
NEXT_PUBLIC_
为前缀的环境变量公开给客户端。了解更多。¥By default, environment variables are only available on the Server. Next.js exposes any environment variable prefixed with
NEXT_PUBLIC_
to the client. Learn more.默认情况下,函数和类已被阻止传递给客户端组件。
¥Functions and classes are already blocked from being passed to Client Components by default.
防止客户端执行仅服务端代码
¥Preventing client-side execution of server-only code
为防止在客户端执行仅服务端代码,你可以使用 server-only
包标记模块:
¥To prevent server-only code from being executed on the client, you can mark a module with the server-only
package:
npm install server-only
yarn add server-only
pnpm add server-only
import 'server-only'
//...
这确保专有代码或内部业务逻辑在客户端环境中导入模块时不会引发构建错误,从而保留在服务器上。
¥This ensures that proprietary code or internal business logic stays on the server by causing a build error if the module is imported in the client environment.
数据修改
¥Mutating Data
Next.js 使用 服务器操作 处理数据突变。
¥Next.js handles mutations with Server Actions.
内置服务器操作安全功能
¥Built-in Server Actions Security features
默认情况下,当创建和导出服务器操作时,它会创建一个公共 HTTP 端点,并且应该使用相同的安全假设和授权检查来处理。这意味着,即使服务器操作或实用程序函数未在代码的其他位置导入,它仍然可以公开访问。
¥By default, when a Server Action is created and exported, it creates a public HTTP endpoint and should be treated with the same security assumptions and authorization checks. This means, even if a Server Action or utility function is not imported elsewhere in your code, it's still publicly accessible.
为了提高安全性,Next.js 具有以下内置功能:
¥To improve security, Next.js has the following built-in features:
安全操作 ID:Next.js 创建加密的非确定性 ID,以允许客户端引用和调用服务器操作。为了增强安全性,这些 ID 会在构建之间定期重新计算。
¥Secure action IDs: Next.js creates encrypted, non-deterministic IDs to allow the client to reference and call the Server Action. These IDs are periodically recalculated between builds for enhanced security.
死代码消除:未使用的服务器操作(通过其 ID 引用)将从客户端包中移除,以避免公开访问。
¥Dead code elimination: Unused Server Actions (referenced by their IDs) are removed from client bundle to avoid public access.
需要了解:
¥Good to know:
ID 是在编译期间创建的,最多缓存 14 天。当启动新构建或构建缓存失效时,它们将被重新生成。此安全改进降低了缺少身份验证层的情况下的风险。但是,你仍应将服务器操作视为公共 HTTP 端点。
¥The IDs are created during compilation and are cached for a maximum of 14 days. They will be regenerated when a new build is initiated or when the build cache is invalidated. This security improvement reduces the risk in cases where an authentication layer is missing. However, you should still treat Server Actions like public HTTP endpoints.
验证客户端输入
¥Validating client input
你应该始终验证来自客户端的输入,因为它们很容易被修改。例如,表单数据、URL 参数、标头和搜索参数:
¥You should always validate input from client, as they can be easily modified. For example, form data, URL parameters, headers, and searchParams:
// BAD: Trusting searchParams directly
export default async function Page({ searchParams }) {
const isAdmin = searchParams.get('isAdmin')
if (isAdmin === 'true') {
// Vulnerable: relies on untrusted client data
return <AdminPanel />
}
}
// GOOD: Re-verify every time
import { cookies } from 'next/headers'
import { verifyAdmin } from './auth'
export default async function Page() {
const token = cookies().get('AUTH_TOKEN')
const isAdmin = await verifyAdmin(token)
if (isAdmin) {
return <AdminPanel />
}
}
认证与授权
¥Authentication and authorization
你应该始终确保用户有权执行操作。例如:
¥You should always ensure that a user is authorized to perform an action. For example:
'use server'
import { auth } from './lib'
export function addItem() {
const { user } = auth()
if (!user) {
throw new Error('You must be signed in to perform this action')
}
// ...
}
在 Next.js 中了解有关 验证 的更多信息。
¥Learn more about Authentication in Next.js.
闭包和加密
¥Closures and encryption
在组件内部定义服务器操作会创建一个 closure,其中该操作可以访问外部函数的范围。例如,publish
操作可以访问 publishVersion
变量:
¥Defining a Server Action inside a component creates a closure where the action has access to the outer function's scope. For example, the publish
action has access to the publishVersion
variable:
export default async function Page() {
const publishVersion = await getLatestVersion();
async function publish() {
"use server";
if (publishVersion !== await getLatestVersion()) {
throw new Error('The version has changed since pressing publish');
}
...
}
return (
<form>
<button formAction={publish}>Publish</button>
</form>
);
}
当你需要在渲染时捕获数据快照(例如 publishVersion
)以便稍后在调用操作时使用它时,闭包非常有用。
¥Closures are useful when you need to capture a snapshot of data (e.g. publishVersion
) at the time of rendering so that it can be used later when the action is invoked.
然而,为了实现这一点,捕获的变量将被发送到客户端,并在调用操作时返回到服务器。为了防止敏感数据暴露给客户端,Next.js 自动对封闭变量进行加密。每次构建 Next.js 应用时,都会为每个操作生成一个新的私钥。这意味着只能针对特定构建调用操作。
¥However, for this to happen, the captured variables are sent to the client and back to the server when the action is invoked. To prevent sensitive data from being exposed to the client, Next.js automatically encrypts the closed-over variables. A new private key is generated for each action every time a Next.js application is built. This means actions can only be invoked for a specific build.
需要了解:我们不建议仅依靠加密来防止敏感值暴露在客户端上。
¥Good to know: We don't recommend relying on encryption alone to prevent sensitive values from being exposed on the client.
覆盖加密密钥(高级)
¥Overwriting encryption keys (advanced)
当跨多个服务器自托管 Next.js 应用时,每个服务器实例最终可能会使用不同的加密密钥,从而导致潜在的不一致。
¥When self-hosting your Next.js application across multiple servers, each server instance may end up with a different encryption key, leading to potential inconsistencies.
为了缓解这种情况,你可以使用 process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY
环境变量覆盖加密密钥。指定此变量可确保你的加密密钥在各个版本中保持不变,并且所有服务器实例都使用相同的密钥。此变量必须是 AES-GCM 加密的。
¥To mitigate this, you can overwrite the encryption key using the process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY
environment variable. Specifying this variable ensures that your encryption keys are persistent across builds, and all server instances use the same key. This variable must be AES-GCM encrypted.
这是一个高级用例,其中跨多个部署的一致加密行为对于你的应用至关重要。你应该考虑标准安全实践,例如密钥轮换和签名。
¥This is an advanced use case where consistent encryption behavior across multiple deployments is critical for your application. You should consider standard security practices such key rotation and signing.
需要了解:部署到 Vercel 的 Next.js 应用会自动处理此问题。
¥Good to know: Next.js applications deployed to Vercel automatically handle this.
允许的来源(高级)
¥Allowed origins (advanced)
由于服务器操作可以在 <form>
元素中调用,因此这将它们打开到 CSRF 攻击。
¥Since Server Actions can be invoked in a <form>
element, this opens them up to CSRF attacks.
在幕后,服务器操作使用 POST
方法,并且只有此 HTTP 方法才允许调用它们。这可以防止现代浏览器中的大多数 CSRF 漏洞,特别是在 同站点 cookies 为默认浏览器的情况下。
¥Behind the scenes, Server Actions use the POST
method, and only this HTTP method is allowed to invoke them. This prevents most CSRF vulnerabilities in modern browsers, particularly with SameSite cookies being the default.
作为额外的保护,Next.js 中的服务器操作还会将 来源标头 与 主机头(或 X-Forwarded-Host
)进行比较。如果这些不匹配,请求将被中止。换句话说,服务器操作只能在与承载它的页面相同的主机上调用。
¥As an additional protection, Server Actions in Next.js also compare the Origin header to the Host header (or X-Forwarded-Host
). If these don't match, the request will be aborted. In other words, Server Actions can only be invoked on the same host as the page that hosts it.
对于使用反向代理或多层后端架构(其中服务器 API 与生产域不同)的大型应用,建议使用配置选项 serverActions.allowedOrigins
选项来指定安全来源列表。该选项接受字符串数组。
¥For large applications that use reverse proxies or multi-layered backend architectures (where the server API differs from the production domain), it's recommended to use the configuration option serverActions.allowedOrigins
option to specify a list of safe origins. The option accepts an array of strings.
了解有关 安全和服务器操作 的更多信息。
¥Learn more about Security and Server Actions.
避免渲染过程中的副作用
¥Avoiding side-effects during rendering
无论是在服务器组件还是客户端组件中,变更(例如注销用户、更新数据库、使缓存无效)都不应成为副作用。Next.js 明确禁止在渲染方法中设置 Cookie 或触发缓存重新验证,以避免出现意外的副作用。
¥Mutations (e.g. logging out users, updating databases, invalidating caches) should never be a side-effect, either in Server or Client Components. Next.js explicitly prevents setting cookies or triggering cache revalidation within render methods to avoid unintended side effects.
// BAD: Triggering a mutation during rendering
export default async function Page({ searchParams }) {
if (searchParams.get('logout')) {
cookies().delete('AUTH_TOKEN')
}
return <UserProfile />
}
你应该使用服务器操作来处理变更。
¥Instead, you should use Server Actions to handle mutations.
// GOOD: Using Server Actions to handle mutations
import { logout } from './actions'
export default function Page() {
return (
<>
<UserProfile />
<form action={logout}>
<button type="submit">Logout</button>
</form>
</>
)
}
需要了解:Next.js 使用
POST
请求来处理变更。这可以防止 GET 请求的意外副作用,从而降低跨站请求伪造 (CSRF) 风险。¥Good to know: Next.js uses
POST
requests to handle mutations. This prevents accidental side-effects from GET requests, reducing Cross-Site Request Forgery (CSRF) risks.
审计
¥Auditing
如果你正在审核 Next.js 项目,我们建议你额外注意以下几点:
¥If you're doing an audit of a Next.js project, here are a few things we recommend looking extra at:
数据访问层:是否有隔离数据访问层的既定做法?请验证数据库包和环境变量是否未在数据访问层之外导入。
¥Data Access Layer: Is there an established practice for an isolated Data Access Layer? Verify that database packages and environment variables are not imported outside the Data Access Layer.
"use client"
文件:组件 props 是否需要私有数据?类型签名是否过于宽泛?¥
"use client"
files: Are the Component props expecting private data? Are the type signatures overly broad?"use server"
文件:Action 参数是在操作中还是在数据访问层内验证的?用户是否在操作内部重新授权?¥
"use server"
files: Are the Action arguments validated in the action or inside the Data Access Layer? Is the user re-authorized inside the action?/[param]/.
带括号的文件夹是用户输入。参数是否经过验证?¥
/[param]/.
Folders with brackets are user input. Are params validated?middleware.tsx
和route.tsx
:功能强大。使用传统技术需要花费额外的时间审核这些内容。定期或根据团队的软件开发生命周期执行渗透测试或漏洞扫描。¥
middleware.tsx
androute.tsx
: Have a lot of power. Spend extra time auditing these using traditional techniques. Perform Penetration Testing or Vulnerability Scanning regularly or in alignment with your team's software development lifecycle.