DEV Community

Cover image for Exploit Analysis: PostgreSQL COPY FROM Filesystem Access
Ofri Peretz
Ofri Peretz

Posted on • Edited on • Originally published at ofriperetz.dev

Exploit Analysis: PostgreSQL COPY FROM Filesystem Access

When PostgreSQL reads your filesystem, your server is compromised. Here is the expliot analysis of COPY FROM and the static analysis standard to block unauthorized filesystem access.

PostgreSQL's COPY FROM is powerful. It can bulk load data from files.

It can also read /etc/passwd.

The Attack

// ❌ User controls file path
const filepath = req.body.filepath;
await client.query(`COPY users FROM '${filepath}'`);
Enter fullscreen mode Exit fullscreen mode

Attacker input:

filepath: /etc/passwd
Enter fullscreen mode Exit fullscreen mode

PostgreSQL now reads your system files into the database.

Security References

This vulnerability is well-documented in industry security standards:

Standard Reference Description
CWE-73 External Control of File Name or Path Application allows external input to control file paths
CWE-22 Path Traversal Improper limitation of pathname to restricted directory
CVE-2019-9193 PostgreSQL COPY FROM PROGRAM Arbitrary code execution via COPY FROM PROGRAM (PostgreSQL 9.3-11.2)
OWASP A03:2021 Injection Injection attacks including file path manipulation

⚠️ Note: While PostgreSQL considers CVE-2019-9193 a "feature" for superusers, the underlying pattern of user-controlled file paths in application code remains a critical vulnerability.

What Can Be Read

Target Impact
/etc/passwd User enumeration
/etc/shadow Password hashes (if accessible)
Application config files Secrets, database credentials
.env files All environment secrets
SSH keys Server access
Application source code Logic, vulnerabilities

The Correct Pattern

// ✅ 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}'`);

// ✅ Or use COPY FROM STDIN with validated data
const stream = client.query(pgCopyStreams.from('COPY users FROM STDIN CSV'));
// Pipe validated CSV data to stream
Enter fullscreen mode Exit fullscreen mode

COPY TO is Also Dangerous

// ❌ Attacker can write to filesystem
await client.query(`COPY users TO '/var/www/html/shell.php'`);
Enter fullscreen mode Exit fullscreen mode

Combined with control over data, this enables:

  • Web shell deployment
  • Configuration file overwrite
  • Cron job injection

The Rule: pg/no-unsafe-copy-from

This pattern is detected by the pg/no-unsafe-copy-from rule from eslint-plugin-pg. The rule uses tiered detection:

Detection Type Severity Triggered By
Dynamic Path 🔒 CRITICAL Template literals with ${var}, string concatenation with variables
Hardcoded Path ⚠️ MEDIUM Literal file paths (operational risk, not injection)
STDIN ✅ Valid COPY FROM STDIN patterns

Let ESLint Catch This

npm install --save-dev eslint-plugin-pg
Enter fullscreen mode Exit fullscreen mode

Use Recommended Config

import pg from 'eslint-plugin-pg';
export default [pg.configs.recommended];
Enter fullscreen mode Exit fullscreen mode

Enable Only This Rule

import pg from 'eslint-plugin-pg';

export default [
  {
    plugins: { pg },
    rules: {
      'pg/no-unsafe-copy-from': 'error',
    },
  },
];
Enter fullscreen mode Exit fullscreen mode

Configure for Admin Scripts

If you have legitimate admin/migration scripts that use hardcoded file paths:

export default [
  {
    files: ['**/migrations/**', '**/scripts/**'],
    rules: {
      'pg/no-unsafe-copy-from': ['error', { allowHardcodedPaths: true }],
    },
  },
];
Enter fullscreen mode Exit fullscreen mode

Allow Specific Paths

export default [
  {
    rules: {
      'pg/no-unsafe-copy-from': [
        'error',
        { allowedPaths: ['^/var/imports/', '\\.csv$'] },
      ],
    },
  },
];
Enter fullscreen mode Exit fullscreen mode

What You'll See

Dynamic Path (CRITICAL - Injection Risk)

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.
Enter fullscreen mode Exit fullscreen mode

Hardcoded Path (MEDIUM - Operational Risk)

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.
Enter fullscreen mode Exit fullscreen mode

Before/After: Fixing the Lint Error

❌ Before (Triggers Lint Error)

// This code triggers pg/no-unsafe-copy-from
const filepath = req.body.filepath;
await client.query(`COPY users FROM '${filepath}'`);
Enter fullscreen mode Exit fullscreen mode

✅ After (Lint Error Resolved)

// 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();
  }
}
Enter fullscreen mode Exit fullscreen mode

Key changes:

  • Replaced COPY FROM '/path/to/file' with COPY FROM STDIN
  • Data now flows through your application, not the filesystem
  • You control validation before it reaches the database

Quick Install

npm install --save-dev eslint-plugin-pg
Enter fullscreen mode Exit fullscreen mode
import pg from 'eslint-plugin-pg';
export default [pg.configs.recommended];
Enter fullscreen mode Exit fullscreen mode

Keep PostgreSQL in the database, not in your filesystem.


📦 npm: eslint-plugin-pg
📖 Rule docs: no-unsafe-copy-from

⭐ Star on GitHub


The Interlace ESLint Ecosystem
Interlace is a high-fidelity suite of static code analyzers designed to automate security, performance, and reliability for the modern Node.js stack. With over 330 rules across 18 specialized plugins, it provides 100% coverage for OWASP Top 10, LLM Security, and Database Hardening.

Explore the full Documentation

© 2026 Ofri Peretz. All rights reserved.


Build Securely.
I'm Ofri Peretz, a Security Engineering Leader and the architect of the Interlace Ecosystem. I build static analysis standards that automate security and performance for Node.js fleets at scale.

ofriperetz.dev | LinkedIn | GitHub

Top comments (0)