文章目录
-
- [1. 概述](#1. 概述)
-
- [1.1 什么是目录遍历(Directory Traversal)?](#1.1 什么是目录遍历(Directory Traversal)?)
- [1.2 核心问题](#1.2 核心问题)
- [1.3 影响范围](#1.3 影响范围)
- [1.4 CWE 分类](#1.4 CWE 分类)
- [1.5 OWASP 排名](#1.5 OWASP 排名)
- [2. 漏洞原理](#2. 漏洞原理)
-
- [2.1 正常行为 vs 攻击行为](#2.1 正常行为 vs 攻击行为)
- [2.2 根本原因](#2.2 根本原因)
- [2.3 Windows vs Linux 差异](#2.3 Windows vs Linux 差异)
- [3. 攻击类型](#3. 攻击类型)
-
- [3.1 基础目录穿越(Basic Path Traversal)](#3.1 基础目录穿越(Basic Path Traversal))
- [3.2 目录列举穿越(Directory Listing)](#3.2 目录列举穿越(Directory Listing))
- [3.3 文件上传 + 目录穿越(Upload with Path Traversal)](#3.3 文件上传 + 目录穿越(Upload with Path Traversal))
- [3.4 数据库路径穿越(Database Path Traversal)](#3.4 数据库路径穿越(Database Path Traversal))
- [3.5 绝对路径穿越(Absolute Path Traversal)](#3.5 绝对路径穿越(Absolute Path Traversal))
- [3.6 压缩包目录穿越(Zip Slip)](#3.6 压缩包目录穿越(Zip Slip))
- [4. 绕过技术](#4. 绕过技术)
-
- [4.1 简单字符串过滤绕过](#4.1 简单字符串过滤绕过)
- [4.2 URL 编码绕过](#4.2 URL 编码绕过)
- [4.3 16 进制 / Unicode 绕过](#4.3 16 进制 / Unicode 绕过)
- [4.4 路径规范化差异绕过](#4.4 路径规范化差异绕过)
- [4.5 符号链接攻击](#4.5 符号链接攻击)
- [4.6 Null Byte 注入(已过时)](#4.6 Null Byte 注入(已过时))
- [5. 防御方案](#5. 防御方案)
-
- [5.1 核心防御函数](#5.1 核心防御函数)
- [5.2 各演示的漏洞 vs 防御对比](#5.2 各演示的漏洞 vs 防御对比)
- [5.3 多层防御汇总](#5.3 多层防御汇总)
- [5.4 Node.js 特定注意事项](#5.4 Node.js 特定注意事项)
- [6. 代码演示](#6. 代码演示)
-
- [6.1 技术栈](#6.1 技术栈)
- [6.2 项目结构](#6.2 项目结构)
- [6.3 数据库结构](#6.3 数据库结构)
- [6.4 演示内容](#6.4 演示内容)
- [6.5 启动方式](#6.5 启动方式)
面向初学者的交互式学习文档 ------ 理解漏洞原理,掌握防御方法
配套演示项目:
DirectoryTraversalDemos(Node.js + Express + PostgreSQL)
1. 概述
1.1 什么是目录遍历(Directory Traversal)?
目录遍历 (又称路径穿越 、目录穿越 ,英文:Directory Traversal / Path Traversal)是一种 Web 安全漏洞,攻击者通过操纵文件路径中的 ../(或 ..\)序列,突破应用程序预期的目录范围,访问服务器文件系统上不应该被访问的文件和目录。
1.2 核心问题
用户输入的 "文件名" → 被直接拼接到文件路径中 → 攻击者注入 "../" 跳出限制目录
一句话总结: 程序把用户输入当作"路径"的一部分拼接了,而不是当作"文件名"来处理。
1.3 影响范围
| 风险等级 | 危害 |
|---|---|
| 高危 | 读取系统配置文件(如 /etc/passwd)、源代码、数据库凭证 |
| 严重 | 读取到数据库密码后 → 数据库脱库 |
| 严重 | 读取到密钥文件后 → 伪造身份、解密敏感数据 |
| 严重 | 结合文件上传 → 写入 Webshell → 远程代码执行(RCE) |
1.4 CWE 分类
- CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
- CWE-23: Relative Path Traversal
- CWE-36: Absolute Path Traversal
1.5 OWASP 排名
连续多年位列 OWASP Top 10 ,在 OWASP Top 10:2021 中属于 A01:2021 -- Broken Access Control(访问控制失效)。
2. 漏洞原理
2.1 正常行为 vs 攻击行为
本演示项目中,服务器将 documents/ 目录作为允许访问的基础目录。
正常场景: 用户请求查看文档 manual.txt,服务器拼接路径:
基础目录: D:\Programs\Security\DirectoryTraversalDemos\documents\
用户输入: manual.txt
最终路径: D:\Programs\Security\DirectoryTraversalDemos\documents\manual.txt 安全
攻击场景: 攻击者输入 ../server.js:
基础目录: D:\Programs\Security\DirectoryTraversalDemos\documents\
用户输入: ../server.js
最终路径: D:\Programs\Security\DirectoryTraversalDemos\documents\..\server.js
= D:\Programs\Security\DirectoryTraversalDemos\server.js 危险!读取到服务器源代码
更严重的攻击: 输入 ../../package.json 可读取项目配置,输入 ../documents/secret.txt 可读取机密文件中的 API 密钥和数据库密码。
2.2 根本原因
以本演示的"演示1"为例,漏洞版本的路由代码(server.js):
javascript
// server.js - 演示1 漏洞版本
app.get("/api/file/vulnerable", (req, res) => {
const filename = req.query.file || "manual.txt";
// 【漏洞核心】用户输入直接拼接到路径中,无任何验证
const filePath = path.join(DOCUMENTS_DIR, filename);
const content = fs.readFileSync(filePath, "utf-8");
// ...
});
问题出在:
- 用户输入被直接拼接到文件系统路径中 ---
path.join(DOCUMENTS_DIR, filename)中的filename来自用户 - 没有验证最终的绝对路径是否仍在允许的目录范围内 --- 没有检查解析后的路径是否还在
documents/下 - 没有过滤路径穿越字符
../、..\
2.3 Windows vs Linux 差异
| 特性 | Linux | Windows |
|---|---|---|
| 路径分隔符 | / |
\ |
| 穿越符号 | ../ |
..\ |
| 敏感文件 | /etc/passwd |
C:\Windows\System32\config\SAM |
| 绝对路径 | /etc/shadow |
C:\boot.ini |
| 空字节截断 | %00(旧版本PHP) |
%00(旧版本ASP) |
注意: Node.js 的
path模块会自动将/转换为当前系统的分隔符,所以在 Node.js 中../在 Windows 上同样生效。本演示在 Windows 上运行时,../和..\效果相同。
3. 攻击类型
3.1 基础目录穿越(Basic Path Traversal)
最简单的攻击形式,直接使用 ../ 序列向上穿越。
本演示对应:演示1 - 基础目录穿越 (GET /api/file/vulnerable?file=xxx)
输入: ../server.js
结果: 读取到项目根目录的 server.js 源代码
典型 Payload(可在演示页面中尝试):
manual.txt → 正常读取用户手册
./secret.txt → 读取 documents 目录下的机密文件(含 API 密钥)
../package.json → 读取项目配置文件
../server.js → 读取服务器源代码
../etc/secret.txt → 穿越到 etc/ 目录读取敏感文件
../documents/config.ini → 穿越后重新读取配置文件
../../SQLInjectionDemos → 读取同级其他项目目录
3.2 目录列举穿越(Directory Listing)
攻击者遍历到父目录后,列出任意目录的文件清单,获取服务器目录结构信息。
本演示对应:演示2 - 目录列举穿越 (GET /api/list/vulnerable?dir=xxx)
输入: ..
结果: 列出 D:\Programs\Security\DirectoryTraversalDemos\ 目录下的所有文件
包括 db/、documents/、etc/、public/、package.json、server.js
典型 Payload:
. → 列出 documents 目录(默认范围)
.. → 列出项目根目录(看到 package.json、server.js)
../.. → 列出上级目录(Security 目录)
../../SQLInjectionDemos → 列出另一个项目目录
3.3 文件上传 + 目录穿越(Upload with Path Traversal)
在上传文件时,文件名中包含 ../ 序列,将文件写入到预期目录之外。
本演示对应:演示3 - 文件上传穿越 (POST /api/upload/vulnerable)
漏洞代码: const maliciousFilename = req.body.filename || req.file.originalname;
const resolvedPath = path.resolve(path.join(UPLOADS_DIR, maliciousFilename));
fs.writeFileSync(resolvedPath, fs.readFileSync(req.file.path));
攻击者构造文件名: ../../etc/hack.html
结果: 文件被写入 etc/ 目录而非 uploads/ 目录
重要说明:
浏览器文件选择器会限制文件名不允许包含
/字符,但这只是前端限制,攻击者可以通过以下方式绕过:
- 使用
curl命令直接构造请求:curl -X POST -F "file=@shell.php;filename=../public/shell.php"- 使用 API 客户端或移动 App 上传(不受浏览器限制)
- 通过代理或中间人攻击修改 HTTP 请求中的文件名
- 上传包含恶意路径的压缩包(Zip Slip 漏洞)
这是 CVSS 评分最高的场景:目录穿越 + 文件上传 = 远程代码执行(RCE)
3.4 数据库路径穿越(Database Path Traversal)
数据库中存储的文件路径被污染,后端从数据库读取路径后直接拼接读取。
本演示对应:演示4 - 数据库路径穿越 (GET /api/dbfile/vulnerable/:id)
本演示的 demo_files 表中,ID=3 的记录存储了恶意路径:
sql
INSERT INTO demo_files (title, filename, file_path, category) VALUES
('系统密码文件', 'secret.txt', '../etc/secret.txt', 'private');
数据库记录 ID=3: file_path = '../etc/secret.txt'
后端代码: path.join(DOCUMENTS_DIR, row.file_path)
最终路径: documents/../etc/secret.txt = etc/secret.txt
为什么数据库会有恶意路径?
- 二次注入: 攻击者通过其他漏洞(如 SQL 注入)修改了文件路径字段
- 数据导入: 从外部数据源导入时包含了恶意路径
- 批量操作: 管理员批量编辑时不小心写入了恶意路径
3.5 绝对路径穿越(Absolute Path Traversal)
当程序没有检查用户输入是否为绝对路径时,攻击者直接提供绝对路径。
输入: C:\Windows\System32\drivers\etc\hosts
结果: 直接读取系统文件(跳过了基础目录拼接)
3.6 压缩包目录穿越(Zip Slip)
解压 ZIP/TAR 等压缩包时,压缩包内的文件名包含 ../ 序列。
压缩包内文件名: ../../.ssh/authorized_keys
解压后路径: /home/user/app/uploads/../../.ssh/authorized_keys
= /home/user/.ssh/authorized_keys
结果: 覆盖用户的 SSH 授权密钥 → 获得 SSH 访问权限
4. 绕过技术
4.1 简单字符串过滤绕过
场景: 开发者过滤了 ../
javascript
// 尝试过滤(不推荐)
filename = filename.replace(/\.\.\//g, "");
绕过:
| Payload | 过滤后 | 结果 |
|---|---|---|
....// |
去掉中间的 ../ → ../ |
绕过成功 |
..././ |
替换后 → ../ |
绕过成功 |
..\/ |
未被匹配 | 绕过成功 |
4.2 URL 编码绕过
攻击者使用 URL 编码来隐藏 ../:
| 编码方式 | Payload |
|---|---|
| 单次编码 | %2e%2e%2f → ../ |
| 双次编码 | %252e%252e%252f → %2e%2e%2f → ../ |
| 混合编码 | ..%2f 或 %2e%2e/ |
| Unicode | ..%c0%af 或 ..%c1%9c |
4.3 16 进制 / Unicode 绕过
%c0%ae%c0%ae%c0%af = ../../
(利用 UTF-8 超长编码)
4.4 路径规范化差异绕过
不同系统的路径规范化行为不同:
Payload: .../.../.../etc/passwd
Windows: unlink 不会移除 ..,导致穿越
Node.js: path.normalize() 可能处理不一致
4.5 符号链接攻击
攻击者在可访问目录中创建符号链接指向目标文件:
bash
ln -s /etc/passwd /var/www/app/uploads/hack.txt
然后请求 /uploads/hack.txt,读取到 /etc/passwd。
4.6 Null Byte 注入(已过时)
旧版 PHP (< 5.3.4) 中,%00 可以截断字符串:
输入: ../../../etc/passwd%00.jpg
PHP处理: ../../../etc/passwd\0.jpg → /etc/passwd
注意: Node.js 不受 Null Byte 注入影响,此方法已基本淘汰。
5. 防御方案
5.1 核心防御函数
本演示项目中,所有安全版本共用一个 validatePath() 函数(定义在 server.js 中):
javascript
function validatePath(userInput, baseDir) {
// 步骤1: 用 path.join 拼接基础目录和用户输入
const fullPath = path.join(baseDir, userInput);
// 步骤2: path.resolve 将路径规范化,消除所有 .. 和 .
const resolvedPath = path.resolve(fullPath);
// 步骤3: 规范化基础目录(处理符号链接等)
const resolvedBase = path.resolve(baseDir);
// 步骤4: 确保规范化后的路径以基础目录 + 分隔符开头
// 加 path.sep 是为了防止 /var/app/documents-fake 这样的目录绕过
if (
!resolvedPath.startsWith(resolvedBase + path.sep) &&
resolvedPath !== resolvedBase
) {
return {
valid: false,
resolvedPath: resolvedPath,
error: `路径穿越被拦截!目标路径 "${resolvedPath}" 不在允许的目录 "${resolvedBase}" 内`,
};
}
return { valid: true, resolvedPath: resolvedPath, error: null };
}
关键要点:
| 步骤 | 代码 | 作用 |
|---|---|---|
| 1. 拼接 | path.join(baseDir, userInput) |
将用户输入与基础目录组合 |
| 2. 规范化 | path.resolve(fullPath) |
消除 ../ 和 .,得到绝对路径 |
| 3. 基准规范化 | path.resolve(baseDir) |
确保基础目录也是绝对路径 |
| 4. 前缀验证 | startsWith(resolvedBase + path.sep) |
确保最终路径仍在允许的目录下 |
为什么加
path.sep? 防止documents-fake这类前缀相同的目录绕过。例如"/var/app/documents-fake".startsWith("/var/app/documents")返回true,但加上path.sep后"/var/app/documents-fake".startsWith("/var/app/documents/")返回false。
5.2 各演示的漏洞 vs 防御对比
演示1:基础目录穿越
场景:文档阅读器,用户输入文件名,服务器从 documents/ 目录读取文件。
javascript
// 漏洞版本 - 直接拼接
app.get("/api/file/vulnerable", (req, res) => {
const filename = req.query.file || "manual.txt";
const filePath = path.join(DOCUMENTS_DIR, filename); // 无任何验证
const content = fs.readFileSync(filePath, "utf-8");
});
// 安全版本 - 路径验证
app.get("/api/file/safe", (req, res) => {
const filename = req.query.file || "manual.txt";
const validation = validatePath(filename, DOCUMENTS_DIR); // 路径验证
if (!validation.valid) {
return res.json({ error: validation.error });
}
const content = fs.readFileSync(validation.resolvedPath, "utf-8");
});
演示效果:
- 漏洞版本输入
../server.js→ 成功读取服务器源代码 - 安全版本输入
../server.js→ 返回"路径穿越被拦截"错误
演示2:目录列举穿越
场景:文件浏览器,用户输入目录名,服务器列出该目录下的文件。
javascript
// 漏洞版本 - 无限制列举
app.get("/api/list/vulnerable", (req, res) => {
const dirname = req.query.dir || ".";
const dirPath = path.join(DOCUMENTS_DIR, dirname); // 直接拼接
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
});
// 安全版本 - 限定目录
app.get("/api/list/safe", (req, res) => {
const dirname = req.query.dir || ".";
const validation = validatePath(dirname, DOCUMENTS_DIR); // 路径验证
if (!validation.valid) {
return res.json({ error: validation.error });
}
const entries = fs.readdirSync(validation.resolvedPath, {
withFileTypes: true,
});
});
演示效果:
- 漏洞版本输入
..→ 列出项目根目录(看到package.json、server.js等) - 安全版本输入
..→ 返回"路径穿越被拦截"错误
演示3:文件上传穿越
场景:文件上传功能,将用户上传的文件保存到 uploads/ 目录。
javascript
// 漏洞版本 - 从请求 body 中获取用户提供的文件名
app.post("/api/upload/vulnerable", multer.single("file"), (req, res) => {
// 【漏洞核心】从请求 body 中获取文件名,直接拼接路径
const maliciousFilename = req.body.filename || req.file.originalname;
const dangerousPath = path.join(UPLOADS_DIR, maliciousFilename);
const resolvedPath = path.resolve(dangerousPath);
// 手动将文件写入目标路径(模拟不安全的文件保存方式)
fs.writeFileSync(resolvedPath, fs.readFileSync(req.file.path));
});
// 安全版本 - UUID 重命名
const safeStorage = multer.diskStorage({
destination: (req, file, cb) => cb(null, UPLOADS_DIR),
filename: (req, file, cb) => {
const ext = path.extname(file.originalname);
const safeName = crypto.randomUUID() + ext; // 完全忽略原始文件名
cb(null, safeName);
},
});
演示效果:
- 漏洞版本:选择任意文件,输入恶意文件名(如
../../etc/hack.html)→ 文件被写入到指定路径 - 安全版本:上传文件 → 文件名被替换为 UUID(如
a1b2c3d4-e5f6-7890-abcd-ef1234567890.txt),路径穿越攻击无效
注意: 浏览器文件选择器不允许文件名包含
/字符。漏洞版本通过手动输入文件名模拟攻击者构造的恶意文件名,真实攻击可通过curl、API 客户端或修改 HTTP 请求实现。
演示4:数据库路径穿越
场景:文档管理系统,demo_files 表中存储文件路径,用户通过 ID 读取文件。
javascript
// 漏洞版本 - 数据库路径直接拼接
app.get("/api/dbfile/vulnerable/:id", async (req, res) => {
const result = await pool.query(
"SELECT id, title, filename, file_path, category FROM demo_files WHERE id = $1",
[req.params.id],
);
const row = result.rows[0];
const filePath = path.join(DOCUMENTS_DIR, row.file_path); // 直接拼接数据库路径
const content = fs.readFileSync(filePath, "utf-8");
});
// 安全版本 - 对数据库路径也做验证
app.get("/api/dbfile/safe/:id", async (req, res) => {
const result = await pool.query(
"SELECT id, title, filename, file_path, category FROM demo_files WHERE id = $1",
[req.params.id],
);
const row = result.rows[0];
const validation = validatePath(row.file_path, DOCUMENTS_DIR); // 路径验证
if (!validation.valid) {
return res.json({ error: validation.error });
}
const content = fs.readFileSync(validation.resolvedPath, "utf-8");
});
演示效果:
- 漏洞版本输入 ID=5 → 数据库中存储的路径
../../../etc/passwd被直接拼接,尝试穿越读取系统文件 - 安全版本输入 ID=5 → 路径验证拦截穿越攻击,返回"路径穿越被拦截"错误
5.3 多层防御汇总
| 防御层 | 方法 | 作用 | 本项目使用 |
|---|---|---|---|
| 路径规范 | path.resolve() + startsWith 检查 |
阻止路径穿越 | 演示1/2/4 |
| UUID 重命名 | crypto.randomUUID() |
消除文件名攻击面 | 演示3 |
| 最小权限 | 应用以低权限用户运行 | 限制可读取的文件范围 | 建议部署时 |
| Chroot | 限制应用的文件系统可见范围 | 即使穿越也无法访问系统文件 | 建议部署时 |
5.4 Node.js 特定注意事项
javascript
// 陷阱1: path.join 对绝对路径的处理
path.join("/safe/dir", "/etc/passwd");
// Windows: \safe\dir\etc\passwd (拼接了)
// Linux: /safe/dir/etc/passwd (拼接了)
// 但如果 userInput 本身就是绝对路径,在某些框架中可能被直接使用
// 陷阱2: startsWith 的前缀匹配问题
"/var/app/documents-fake/secret.txt".startsWith("/var/app/documents"); // true!
// 解决方案:拼接 path.sep
"/var/app/documents-fake/secret.txt".startsWith(
"/var/app/documents" + path.sep,
); // false
// 正确做法
const baseDir = path.resolve("/safe/dir");
const target = path.resolve(path.join(baseDir, userInput));
if (!target.startsWith(baseDir + path.sep) && target !== baseDir) {
throw new Error("Access denied: path traversal detected");
}
6. 代码演示
本目录下的 DirectoryTraversalDemos 项目提供了完整的可运行演示。
6.1 技术栈
| 组件 | 技术 | 说明 |
|---|---|---|
| 后端框架 | Express 4.x | Node.js 最流行的 Web 框架 |
| 数据库 | PostgreSQL | 通过 pg 驱动连接(演示4使用) |
| 文件上传 | multer 1.x | Express 文件上传中间件(演示3使用) |
| 前端 | 原生 HTML/CSS/JS | 深色主题,4个演示面板,每个都有漏洞/安全对比 |
6.2 项目结构
DirectoryTraversalDemos/
├── 目录遍历与目录穿越漏洞详解.md ← 本文档
├── package.json ← 项目配置(express + pg + multer)
├── server.js ← 主服务器(含 8 个路由:4 漏洞 + 4 安全)
├── db/
│ ├── pool.js ← PostgreSQL 连接池配置
│ └── init.sql ← 数据库初始化脚本(建表 + 插入测试数据)
├── documents/ ← 允许访问的文档目录(基础目录)
│ ├── manual.txt ← 用户手册
│ ├── config.ini ← 配置文件(含模拟密钥:api_key、admin_password)
│ └── secret.txt ← 机密文件(含 API 密钥、数据库密码)
├── etc/ ← 模拟系统敏感文件目录(用于演示穿越攻击目标)
│ └── secret.txt ← 敏感文件(模拟系统级机密)
├── uploads/ ← 文件上传目标目录(服务器运行时自动创建)
└── public/
├── index.html ← 演示首页(4 个演示面板 + JavaScript 交互逻辑)
└── style.css ← 深色主题样式表
6.3 数据库结构
sql
-- demo_files 表:存储文档元数据和文件路径
CREATE TABLE demo_files (
id SERIAL PRIMARY KEY,
title VARCHAR(200) NOT NULL, -- 文档标题
filename VARCHAR(500) NOT NULL, -- 文件名
file_path VARCHAR(500) NOT NULL, -- 文件路径(可能包含恶意路径)
category VARCHAR(50) DEFAULT 'public' -- 分类:public / private / internal
);
-- 测试数据(ID=5 的路径为恶意数据,用于演示数据库路径穿越)
INSERT INTO demo_files (title, filename, file_path, category) VALUES
('系统使用手册', 'manual.txt', 'documents/manual.txt', 'public'),
('配置说明', 'config.ini', 'documents/config.ini', 'public'),
('内部备忘录', 'memo.txt', 'documents/internal/memo.txt', 'internal'),
('数据库连接配置', 'database.properties', '/etc/secret/database.properties', 'private'),
('系统密码文件', 'passwd.txt', '../../../etc/passwd', 'private'); -- 恶意数据
6.4 演示内容
| 编号 | 演示名称 | 漏洞路由 | 安全路由 | 漏洞核心 | 防御方法 |
|---|---|---|---|---|---|
| 1 | 基础目录穿越 | GET /api/file/vulnerable?file=xxx |
GET /api/file/safe?file=xxx |
path.join() 直接拼接 |
validatePath() 路径验证 |
| 2 | 目录列举穿越 | GET /api/list/vulnerable?dir=xxx |
GET /api/list/safe?dir=xxx |
无限制目录列举 | 路径前缀验证 |
| 3 | 文件上传穿越 | POST /api/upload/vulnerable |
POST /api/upload/safe |
使用 file.originalname |
crypto.randomUUID() 重命名 |
| 4 | 数据库路径穿越 | GET /api/dbfile/vulnerable/:id |
GET /api/dbfile/safe/:id |
数据库路径直接拼接 | 路径验证 + 权限控制 |
6.5 启动方式
bash
cd DirectoryTraversalDemos
# 1. 创建数据库
psql -U postgres -c "CREATE DATABASE directory_traversal_demos;"
# 2. 初始化数据表
psql -U postgres -d directory_traversal_demos -f db/init.sql
# 3. 安装依赖
npm install
# 4. 启动服务(端口 3001)
npm start
# 5. 访问 http://localhost:3001
演示 1-3 不依赖数据库,可直接使用。演示 4 需要 PostgreSQL 运行中。
最后提醒: 本文档和演示代码仅供安全教学和研究使用。在生产环境中,务必对用户输入进行严格的路径验证和过滤,遵循最小权限原则。