OPEN HYPER STEP
← 목록으로 (stack-analysis)
STACK-ANALYSIS · 108 / 120
stack-analysis
CHAPTER 108 / 120
읽기 약 2
FUNCTION

파일 업로드 보안: 검증 + 격리 + 스캔


핵심 개념

magic byte·MIME·격리 스토리지·바이러스 스캔 — 안전한 업로드.

본문

검증 4단계

📋 코드 (4줄)
1. 확장자 검증 (1차)
2. MIME type 검증 (2차)
3. Magic Byte 검증 (3차) — 가장 신뢰
4. 콘텐츠 검증 (4차) — 실제 처리

Magic Byte 검증

TYPESCRIPT📋 코드 (27줄)
import { fileTypeFromBuffer } from 'file-type';

async function validateUpload(buffer: Buffer, mime: string) {
  // 1. file-type으로 실제 타입 감지
  const detected = await fileTypeFromBuffer(buffer);
  if (!detected) {
    throw new Error('Cannot detect file type');
  }

  // 2. 클라이언트가 보낸 MIME과 일치?
  if (detected.mime !== mime) {
    throw new Error(`MIME mismatch: ${mime} vs ${detected.mime}`);
  }

  // 3. 화이트리스트
  const ALLOWED = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf'];
  if (!ALLOWED.includes(detected.mime)) {
    throw new Error('File type not allowed');
  }

  // 4. 크기
  if (buffer.length > 10 * 1024 * 1024) {
    throw new Error('File too large');
  }

  return detected;
}

격리 스토리지

TYPESCRIPT📋 코드 (21줄)
// ❌ 같은 도메인에 호스팅 → XSS 위험
// example.com/uploads/user.html (악성)


// ✅ 별도 도메인 + Sandbox
// uploads.example.com 또는 cdn.example.com


// next.config.js
module.exports = {
  images: {
    remotePatterns: [
      { protocol: 'https', hostname: 'cdn.example.com' },
    ],
  },
};


// 응답 헤더 — 강제 다운로드
res.setHeader('Content-Disposition', 'attachment; filename="user-file.pdf"');
res.setHeader('X-Content-Type-Options', 'nosniff');

S3 + Presigned URL (안전)

TYPESCRIPT📋 코드 (36줄)
import { PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

app.post('/upload-url', authenticate, async (req, res) => {
  const { filename, contentType, size } = req.body;

  // 검증
  if (size > 10 * 1024 * 1024) return res.status(400).end();
  if (!ALLOWED_MIMES.includes(contentType)) return res.status(400).end();

  // 사용자 폴더로 격리
  const key = `users/${req.user.id}/${crypto.randomUUID()}-${sanitizeFilename(filename)}`;

  const command = new PutObjectCommand({
    Bucket: PRIVATE_BUCKET,        // public 노출 X
    Key: key,
    ContentType: contentType,
    ContentLength: size,           // 크기 제한
    Metadata: {
      uploaderId: req.user.id,
      uploadedAt: new Date().toISOString(),
    },
    ServerSideEncryption: 'AES256',
  });

  const url = await getSignedUrl(s3, command, { expiresIn: 300 });  // 5분
  res.json({ uploadUrl: url, key });
});


function sanitizeFilename(name: string) {
  return name
    .replace(/[^a-zA-Z0-9._-]/g, '_')
    .replace(/\.+/g, '.')
    .slice(0, 100);
}

바이러스 스캔

TYPESCRIPT📋 코드 (32줄)
// ClamAV (오픈소스)
import { NodeClam } from 'clamscan';

const clamscan = await new NodeClam().init({
  clamdscan: { socket: '/var/run/clamav/clamd.ctl' },
});


async function scanFile(buffer: Buffer) {
  const result = await clamscan.scanStream(Buffer.from(buffer));
  if (result.isInfected) {
    throw new Error(`Virus detected: ${result.viruses.join(', ')}`);
  }
}


// 또는 VirusTotal API
async function scanWithVT(buffer: Buffer) {
  const formData = new FormData();
  formData.append('file', new Blob([buffer]));

  const res = await fetch('https://www.virustotal.com/api/v3/files', {
    method: 'POST',
    headers: { 'x-apikey': process.env.VT_API_KEY! },
    body: formData,
  });
  return res.json();
}


// 또는 AWS S3 — Bucket Antivirus
// S3 + Lambda + ClamAV (Trend Micro 솔루션)

이미지 — 메타데이터 제거

TYPESCRIPT📋 코드 (12줄)
import sharp from 'sharp';

async function processImage(buffer: Buffer) {
  return sharp(buffer)
    .rotate()           // EXIF 회전 적용
    .removeMetadata()   // EXIF 제거 (위치 정보 등)
    .webp({ quality: 80 })
    .toBuffer();
}


// EXIF에 GPS 좌표·기기 정보 등 — 프라이버시 보호

SVG 위험 — Sanitize 또는 차단

TYPESCRIPT📋 코드 (16줄)
import DOMPurify from 'isomorphic-dompurify';

async function processSVG(buffer: Buffer) {
  const text = buffer.toString();
  const clean = DOMPurify.sanitize(text, {
    USE_PROFILES: { svg: true, svgFilters: true },
  });
  return Buffer.from(clean);
}


// 또는 단순히 SVG 차단
const ALLOWED_MIMES = [
  'image/jpeg', 'image/png', 'image/webp', 'image/gif',
  // 'image/svg+xml',  // 제외
];

다운로드 시 보안

TYPESCRIPT📋 코드 (19줄)
app.get('/files/:id/download', authenticate, async (req, res) => {
  const file = await db.file.findUnique({ where: { id: req.params.id } });

  // 권한 검증
  if (file!.userId !== req.user.id) return res.status(403).end();

  // S3 presigned URL (다운로드)
  const url = await getSignedUrl(
    s3,
    new GetObjectCommand({
      Bucket: PRIVATE_BUCKET,
      Key: file!.s3Key,
      ResponseContentDisposition: `attachment; filename="${encodeURIComponent(file!.originalName)}"`,
    }),
    { expiresIn: 60 }
  );

  res.redirect(url);
});

Path Traversal 방어

TYPESCRIPT📋 코드 (19줄)
// ❌ 사용자 입력으로 경로 구성
const filePath = `/uploads/${req.params.filename}`;
// ?filename=../../../etc/passwd → 시스템 파일 노출


// ✅ 검증 + 격리
import path from 'path';

const SAFE_DIR = '/var/uploads';
const requested = path.join(SAFE_DIR, req.params.filename);

if (!requested.startsWith(SAFE_DIR)) {
  return res.status(403).end();
}

const safe = path.resolve(requested);
if (!safe.startsWith(path.resolve(SAFE_DIR))) {
  return res.status(403).end();
}

다음 챕터

CH.109 "의존성 보안: npm audit + Snyk + Renovate".


AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude

무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6

내 코드의 파일 업로드 보안 부분을 분석해서
실전 분석 + 개선 우선순위를 알려줘.
ChatGPT

무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro

파일 업로드 보안 관련 베스트 프랙티스 5가지를
비교 분석해서 패턴 추출를 알려줘.
Gemini

무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro

내 프로젝트 전체에서 파일 업로드 보안
최적화 가능 위치를 보고해줘.
Grok

무료: Grok 4.1 / SuperGrok $30/mo

2026년 한국 시장의 파일 업로드 보안
트렌드를 솔직히 알려줘.

⭐ 이것만 기억하세요
파일 업로드 보안: 검증 + 격리 + 스캔 이 3가지만 확실히 잡으세요
1.Magic Byte 검증 = 확장자·MIME 위조 방지 (file-type)
2.별도 CDN 도메인으로 격리 + Content-Disposition: attachment
3.S3 presigned URL + 파일 폴더 격리 + 메타데이터 제거


공유하기
진행도 108 / 120