COPY FROM Exploits: PostgreSQL이 파일 시스템을 읽을 때

발행: (2026년 1월 3일 오전 05:36 GMT+9)
6 분 소요
원문: Dev.to

Source: Dev.to

Cover image for COPY FROM Exploits: When PostgreSQL Reads Your Filesystem

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 PROGRAMCOPY FROM PROGRAM을 통한 임의 코드 실행 (PostgreSQL 9.3‑11.2)
OWASPA03: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‑pgpg/no-unsafe-copy-from 규칙에 의해 감지됩니다. 이 규칙은 단계별 감지를 사용합니다:

감지 유형심각도트리거
Dynamic Path🔒 CRITICAL${var}가 포함된 템플릿 리터럴, 변수와 함께하는 문자열 연결
Hardcoded Path⚠️ MEDIUM리터럴 파일 경로 (운영 위험, 주입은 아님)
STDINVALIDCOPY 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 데이터를 파일 시스템이 아니라 데이터베이스에 보관하세요.

🚀 보안 기사 및 업데이트를 더 받아보려면 팔로우하세요:
GitHub | X | LinkedIn | Dev.to

Back to Blog

관련 글

더 보기 »

RGB LED 사이드퀘스트 💡

markdown !Jennifer Davis https://media2.dev.to/dynamic/image/width=50,height=50,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%...

Mendex: 내가 만드는 이유

소개 안녕하세요 여러분. 오늘은 제가 누구인지, 무엇을 만들고 있는지, 그리고 그 이유를 공유하고 싶습니다. 초기 경력과 번아웃 저는 개발자로서 17년 동안 경력을 시작했습니다.