COPY FROM 利用:当 PostgreSQL 读取你的文件系统时

发布: (2026年1月3日 GMT+8 04:36)
6 min read
原文: Dev.to

Source: Dev.to

COPY FROM Exploits:当 PostgreSQL 读取你的文件系统的封面图片

Ofri Peretz

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‑73External Control of File Name or Path应用允许外部输入控制文件路径
CWE‑22Path Traversal对路径名限制不当,导致可访问受限目录之外的路径
CVE‑2019‑9193PostgreSQL COPY FROM PROGRAM通过 COPY FROM PROGRAM 实现任意代码执行(PostgreSQL 9.3‑11.2)
OWASPA03: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 数据保存在数据库中,而不是在文件系统上。

🚀 关注我获取更多安全文章和更新:
GitHub | X | LinkedIn | Dev.to

Back to Blog

相关文章

阅读更多 »