COPY FROM Exploits: PostgreSQL이 파일 시스템을 읽을 때
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 | 비밀번호 해시 (접근 가능 시) |
| Application config files | 비밀, 데이터베이스 자격 증명 |
.env files | 모든 환경 비밀 |
| SSH keys | 서버 접근 |
| Application source code | 로직, 취약점 |
올바른 패턴
// ✅ 사용자 입력을 파일 경로에 사용하지 않기
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을 사용하세요:
// ✅ COPY FROM STDIN 사용
const stream = client.query(pgCopyStreams.from('COPY users FROM STDIN CSV'));
// 검증된 CSV 데이터를 `stream`에 파이프
COPY TO도 위험합니다
// ❌ Attacker can write to filesystem
await client.query(`COPY users TO '/var/www/html/shell.php'`);
데이터에 대한 제어와 결합될 경우 다음을 가능하게 합니다:
- 웹‑쉘 배포
- 설정 파일 덮어쓰기
- 크론‑작업 주입
규칙: pg/no-unsafe-copy-from
패턴은 eslint‑plugin‑pg의 pg/no-unsafe-copy-from 규칙에 의해 감지됩니다. 이 규칙은 단계별 감지를 사용합니다:
| 감지 유형 | 심각도 | 트리거 |
|---|---|---|
| Dynamic Path | 🔒 CRITICAL | ${var}가 포함된 템플릿 리터럴, 변수와 함께하는 문자열 연결 |
| Hardcoded Path | ⚠️ MEDIUM | 리터럴 파일 경로 (운영 위험, 주입은 아님) |
| STDIN | ✅ VALID | 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$'] },
],
},
},
];
보게 될 내용
동적 경로 (CRITICAL – 인젝션 위험)
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.
하드코딩된 경로 (MEDIUM – 운영 위험)
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.
Before / After: Lint 오류 수정
❌ Before (Lint 오류 발생)
// This code triggers pg/no-unsafe-copy-from
const filepath = req.body.filepath;
await client.query(`COPY users FROM '${filepath}'`);
✅ After (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으로 교체했습니다.- 데이터가 파일 시스템이 아니라 애플리케이션을 통해 흐릅니다.
- 데이터베이스에 도달하기 전에 검증을 직접 제어할 수 있습니다.
Quick Install
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
🚀 보안 기사 및 업데이트를 더 받아보려면 팔로우하세요:
GitHub | X | LinkedIn | Dev.to
