COPY FROM 利用:当 PostgreSQL 读取你的文件系统时
发布: (2026年1月3日 GMT+8 04:36)
6 min read
原文: Dev.to
Source: Dev.to

PostgreSQL 的 COPY FROM 功能强大。它可以从文件批量加载数据,但它也可以读取任意文件,例如 /etc/passwd。
攻击
// ❌ User controls file path
const filepath = req.body.filepath;
await client.query(`COPY users FROM '${filepath}'`);
攻击者输入
filepath: /etc/passwd
PostgreSQL 现在会将系统文件读取到数据库中。
安全参考
| 标准 | 参考 | 描述 |
|---|---|---|
| CWE‑73 | External Control of File Name or Path | 应用允许外部输入控制文件路径 |
| CWE‑22 | Path Traversal | 对路径名限制不当,导致可访问受限目录之外的路径 |
| CVE‑2019‑9193 | PostgreSQL COPY FROM PROGRAM | 通过 COPY FROM PROGRAM 实现任意代码执行(PostgreSQL 9.3‑11.2) |
| OWASP | A03:2021 Injection | 包括文件路径操控在内的注入攻击 |
⚠️ 注意:虽然 PostgreSQL 将 CVE‑2019‑9193 视为超级用户的“特性”,但应用代码中用户可控制的文件路径模式仍然是关键漏洞。
可以读取的内容
| 目标 | 影响 |
|---|---|
/etc/passwd | 用户枚举 |
/etc/shadow | 密码哈希(如果可访问) |
| 应用程序配置文件 | 密钥、数据库凭证 |
.env 文件 | 所有环境密钥 |
| SSH 密钥 | 服务器访问 |
| 应用程序源代码 | 逻辑、漏洞 |
正确的模式
// ✅ Never use user input in file paths
const ALLOWED_IMPORTS = {
users: '/var/imports/users.csv',
products: '/var/imports/products.csv',
};
const filepath = ALLOWED_IMPORTS[req.body.type];
if (!filepath) throw new Error('Invalid import type');
await client.query(`COPY users FROM '${filepath}'`);
或者,使用已验证的数据配合 COPY FROM STDIN:
// ✅ Use COPY FROM STDIN
const stream = client.query(pgCopyStreams.from('COPY users FROM STDIN CSV'));
// Pipe validated CSV data to `stream`
COPY TO 也很危险
// ❌ Attacker can write to filesystem
await client.query(`COPY users TO '/var/www/html/shell.php'`);
结合对数据的控制,这会导致:
- Web‑shell 部署
- 配置文件覆盖
- Cron 任务注入
规则:pg/no-unsafe-copy-from
该模式由 eslint‑plugin‑pg 中的 pg/no-unsafe-copy-from 规则检测。该规则使用分层检测:
| 检测类型 | 严重程度 | 触发条件 |
|---|---|---|
| Dynamic Path | 🔒 关键 | 包含 ${var} 的模板字面量,使用变量的字符串拼接 |
| Hardcoded Path | ⚠️ 中等 | 字面文件路径(运营风险,不是注入) |
| STDIN | ✅ 有效 | COPY FROM STDIN 模式 |
让 ESLint 捕获此问题
npm install --save-dev eslint-plugin-pg
使用推荐配置
import pg from 'eslint-plugin-pg';
export default [pg.configs.recommended];
仅启用此规则
import pg from 'eslint-plugin-pg';
export default [
{
plugins: { pg },
rules: {
'pg/no-unsafe-copy-from': 'error',
},
},
];
为管理脚本配置
如果你有合法的管理员/迁移脚本使用硬编码的文件路径:
export default [
{
files: ['**/migrations/**', '**/scripts/**'],
rules: {
'pg/no-unsafe-copy-from': ['error', { allowHardcodedPaths: true }],
},
},
];
允许特定路径
export default [
{
rules: {
'pg/no-unsafe-copy-from': [
'error',
{ allowedPaths: ['^/var/imports/', '\\.csv$'] },
],
},
},
];
您将看到的内容
动态路径(关键 – 注入风险)
src/import.ts
8:15 error 🔒 CWE-73 OWASP:A03-Injection | Dynamic file path in COPY FROM detected - potential arbitrary file read. | CRITICAL [SOC2,PCI-DSS]
Fix: Never use user input in COPY FROM paths. Use COPY FROM STDIN for user data.
硬编码路径(中等 – 运营风险)
src/import.ts
8:15 warning ⚠️ CWE-73 | Hardcoded file path in COPY FROM - server-side file access. | MEDIUM
Fix: Prefer COPY FROM STDIN for application code. Use allowHardcodedPaths option if this is an admin script.
前后对比:修复 Lint 错误
❌ 前(触发 Lint 错误)
// This code triggers pg/no-unsafe-copy-from
const filepath = req.body.filepath;
await client.query(`COPY users FROM '${filepath}'`);
✅ 后(已解决 Lint 错误)
// Safe implementation – whitelist allowed imports
const ALLOWED_IMPORTS = {
users: '/var/imports/users.csv',
};
const filepath = ALLOWED_IMPORTS[req.body.type];
if (!filepath) throw new Error('Invalid import type');
await client.query(`COPY users FROM '${filepath}'`);
安全的 COPY FROM STDIN 示例
// Use COPY FROM STDIN – the recommended safe pattern
import { from as copyFrom } from 'pg-copy-streams';
import { Readable } from 'stream';
async function importUsers(csvData) {
const client = await pool.connect();
try {
// ✅ COPY FROM STDIN is safe – no file‑system access
const stream = client.query(
copyFrom('COPY users (name, email) FROM STDIN CSV')
);
// Validate and stream the data from your application
const validatedCsv = csvData
.map(row => `${sanitize(row.name)},${sanitize(row.email)}`)
.join('\n');
Readable.from(validatedCsv).pipe(stream);
await new Promise((resolve, reject) => {
stream.on('finish', resolve);
stream.on('error', reject);
});
} finally {
client.release();
}
}
关键更改
- 将
COPY FROM '/path/to/file'替换为COPY FROM STDIN。 - 数据现在通过你的应用程序流动,而不是文件系统。
- 在数据到达数据库之前,你可以进行验证。
快速安装
npm install --save-dev eslint-plugin-pg
import pg from 'eslint-plugin-pg';
export default [pg.configs.recommended];
将 PostgreSQL 数据保存在数据库中,而不是在文件系统上。
- 📦 npm: eslint-plugin-pg
- 📖 规则文档: no-unsafe-copy-from
- ⭐ 在 GitHub 上加星: https://github.com/ofri-peretz/eslint
