본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 9장: 보안, 인증, 권한 관리
2026년 2월 6일·AI / ML·

9장: 보안, 인증, 권한 관리

MCP 서버의 OAuth 2.1 인증, 입력 유효성 검사, 명령 주입 방지, 데이터 유출 방지 등 프로덕션 보안 모범 사례를 다룹니다.

20분836자10개 섹션
mcptypescriptpython
공유
mcp-guide9 / 10
12345678910
이전8장: 기존 시스템과 MCP 연동하기다음10장: 실전 프로젝트 - 풀스택 MCP 시스템 구축

MCP 보안의 중요성

MCP 서버는 데이터베이스, API, 파일 시스템 등 민감한 시스템에 대한 접근 경로를 제공합니다. 보안이 취약한 MCP 서버는 공격자에게 내부 시스템의 문을 열어주는 것과 같습니다.

MCP의 보안 위험은 일반적인 웹 애플리케이션과 다른 특수한 측면이 있습니다. 도구의 입력은 LLM이 생성하는데, LLM은 프롬프트 주입(Prompt Injection) 공격에 의해 조작될 수 있습니다. 또한 MCP 서버가 여러 사용자의 요청을 처리할 때, 한 사용자의 데이터가 다른 사용자에게 노출되는 혼동된 대리인(Confused Deputy) 문제가 발생할 수 있습니다.

이 장에서는 OWASP의 MCP 보안 가이드와 공식 MCP 사양의 보안 권고사항을 바탕으로, 프로덕션 환경에서 반드시 적용해야 할 보안 모범 사례를 다룹니다.

인증(Authentication)

OAuth 2.1 기반 인증

MCP 사양은 원격 서버(Streamable HTTP 전송)에 대한 인증 방식으로 OAuth 2.1을 권장합니다. OAuth 2.1은 기존 OAuth 2.0의 보안 모범 사례를 통합하고, PKCE(Proof Key for Code Exchange)를 필수로 요구하는 등 보안을 강화한 버전입니다.

토큰 검증

MCP 서버는 모든 요청에서 토큰을 검증해야 합니다. 토큰이 존재한다는 것만으로 유효하다고 가정해서는 안 됩니다.

토큰 검증 미들웨어
typescript
import jwt from "jsonwebtoken";
 
interface TokenPayload {
  sub: string;        // 사용자 ID
  aud: string;        // 대상 (이 MCP 서버)
  scope: string;      // 허용된 범위
  exp: number;        // 만료 시간
}
 
function validateToken(token: string): TokenPayload {
  // 1. 토큰 디코딩 및 서명 검증
  const payload = jwt.verify(token, PUBLIC_KEY, {
    algorithms: ["RS256"],
    audience: "mcp-server.example.com",  // 이 서버의 식별자
    issuer: "https://auth.example.com",  // 인증 서버
  }) as TokenPayload;
 
  // 2. 만료 여부 확인
  if (payload.exp < Date.now() / 1000) {
    throw new Error("토큰이 만료되었습니다.");
  }
 
  // 3. 대상(audience) 확인
  if (payload.aud !== "mcp-server.example.com") {
    throw new Error("이 서버를 대상으로 발급된 토큰이 아닙니다.");
  }
 
  return payload;
}
Warning

MCP 서버는 자신을 대상으로 발급되지 않은 토큰을 절대 수락해서는 안 됩니다. 다른 서비스를 위해 발급된 토큰을 수락하면, 공격자가 낮은 권한의 서비스에서 받은 토큰으로 높은 권한의 MCP 서버에 접근할 수 있습니다. 토큰의 aud(audience) 클레임을 반드시 검증하십시오.

세션 관리

Streamable HTTP 전송에서 세션을 사용하는 경우, 세션 ID의 안전한 관리가 중요합니다.

안전한 세션 관리
typescript
import crypto from "crypto";
 
class SessionManager {
  private sessions: Map<string, SessionData> = new Map();
 
  createSession(userId: string): string {
    // 1. 암호학적으로 안전한 랜덤 세션 ID 생성
    const sessionId = crypto.randomUUID();
 
    // 2. 사용자 ID와 세션 ID를 결합하여 키 생성
    const sessionKey = userId + ":" + sessionId;
 
    // 3. 세션 데이터 저장 (TTL 포함)
    this.sessions.set(sessionKey, {
      userId: userId,
      createdAt: Date.now(),
      expiresAt: Date.now() + 3600000,  // 1시간
    });
 
    return sessionId;
  }
 
  validateSession(sessionId: string, userId: string): boolean {
    const sessionKey = userId + ":" + sessionId;
    const session = this.sessions.get(sessionKey);
 
    if (!session) return false;
    if (session.expiresAt < Date.now()) {
      this.sessions.delete(sessionKey);
      return false;
    }
 
    return true;
  }
}

세션 ID를 사용자 ID와 결합하면, 공격자가 세션 ID를 탈취하더라도 해당 사용자의 컨텍스트에서만 동작하도록 제한할 수 있습니다.

인가(Authorization)

인가는 인증된 사용자가 어떤 작업을 수행할 수 있는지를 결정합니다.

도구별 권한 제어

각 도구에 필요한 권한(scope)을 정의하고, 요청 시 사용자의 권한을 검증합니다.

도구별 권한 제어
typescript
const TOOL_PERMISSIONS: Record<string, string[]> = {
  query: ["db:read"],
  execute: ["db:write"],
  create_issue: ["github:write"],
  list_issues: ["github:read"],
  delete_file: ["fs:write", "fs:delete"],
};
 
function checkPermission(
  toolName: string,
  userScopes: string[]
): boolean {
  const required = TOOL_PERMISSIONS[toolName];
  if (!required) return false;
 
  return required.every((scope) => userScopes.includes(scope));
}
 
// 도구 핸들러에서 권한 검증
server.tool("execute", "SQL을 실행합니다", {
  sql: z.string(),
}, async (params, extra) => {
  const token = extractToken(extra);
  const payload = validateToken(token);
 
  if (!checkPermission("execute", payload.scope.split(" "))) {
    return {
      content: [{
        type: "text" as const,
        text: "이 작업에 대한 권한이 없습니다. 필요한 권한: db:write",
      }],
      isError: true,
    };
  }
 
  // 쿼리 실행...
});

세분화된 스코프 설계

넓은 범위의 스코프(예: admin) 대신, 도구 또는 기능 단위의 세분화된 스코프를 정의합니다.

db:read          - 데이터베이스 읽기
db:write         - 데이터베이스 쓰기
db:schema        - 스키마 변경
github:read      - GitHub 조회
github:write     - GitHub 이슈/PR 생성
github:admin     - GitHub 설정 변경
fs:read          - 파일 읽기
fs:write         - 파일 쓰기
fs:delete        - 파일 삭제

입력 유효성 검사

MCP 서버에 전달되는 모든 입력은 잠재적으로 위험합니다. LLM이 프롬프트 주입에 의해 조작되어 악의적인 입력을 전달할 수 있기 때문입니다.

명령 주입 방지

8장에서 다룬 CLI 래핑 서버처럼, 셸 명령을 실행하는 도구는 명령 주입에 특히 취약합니다.

취약한 코드
typescript
// 위험: 사용자 입력을 셸 명령에 직접 삽입
server.tool("list_dir", "디렉토리를 조회합니다", {
  path: z.string(),
}, async (params) => {
  // 공격자가 path에 "; rm -rf /"를 전달하면?
  const { stdout } = await execAsync("ls -la " + params.path);
  return { content: [{ type: "text", text: stdout }] };
});
안전한 코드
typescript
import { execFile } from "child_process";
import { promisify } from "util";
import path from "path";
 
const execFileAsync = promisify(execFile);
 
server.tool("list_dir", "디렉토리를 조회합니다", {
  path: z.string(),
}, async (params) => {
  // 1. 경로 정규화 (경로 탈출 방지)
  const normalizedPath = path.resolve("/allowed/base", params.path);
 
  // 2. 허용된 경로 내인지 확인
  if (!normalizedPath.startsWith("/allowed/base")) {
    return {
      content: [{ type: "text" as const, text: "허용되지 않은 경로입니다." }],
      isError: true,
    };
  }
 
  // 3. execFile 사용 (셸을 거치지 않으므로 명령 주입 불가)
  const { stdout } = await execFileAsync("ls", ["-la", normalizedPath]);
  return { content: [{ type: "text" as const, text: stdout }] };
});

핵심 방어 전략은 세 가지입니다.

  1. exec 대신 execFile 사용: execFile은 셸을 거치지 않으므로 명령 주입이 불가능합니다.
  2. 경로 정규화(Canonicalization): path.resolve()로 경로를 정규화하여 ../ 탈출을 방지합니다.
  3. 화이트리스트 기반 검증: 허용된 경로, 명령, 매개변수만 수락합니다.

SQL 주입 방지

안전한 쿼리 실행
typescript
// 위험: 문자열 연결
const result = await pool.query(
  "SELECT * FROM users WHERE id = '" + params.userId + "'"
);
 
// 안전: 매개변수화된 쿼리
const result = await pool.query(
  "SELECT * FROM users WHERE id = $1",
  [params.userId]
);

경로 탈출(Path Traversal) 방지

경로 탈출 방지
typescript
function sanitizePath(basePath: string, userPath: string): string {
  // 1. 경로 정규화
  const resolved = path.resolve(basePath, userPath);
 
  // 2. 기본 경로 내인지 확인
  if (!resolved.startsWith(path.resolve(basePath))) {
    throw new Error("허용된 경로 범위를 벗어났습니다.");
  }
 
  // 3. 심볼릭 링크 확인 (옵션)
  const real = fs.realpathSync(resolved);
  if (!real.startsWith(path.resolve(basePath))) {
    throw new Error("심볼릭 링크가 허용된 범위를 벗어납니다.");
  }
 
  return resolved;
}

프롬프트 주입 방어

프롬프트 주입(Prompt Injection)은 공격자가 LLM에 악의적인 지시를 삽입하여, LLM이 의도하지 않은 도구 호출을 수행하도록 만드는 공격입니다.

위험 시나리오

방어 전략

도구 수준 방어: 위험한 도구에 확인 단계를 추가합니다.

확인 단계가 포함된 도구
typescript
server.tool(
  "delete_file",
  "파일을 삭제합니다. 되돌릴 수 없는 작업입니다.",
  {
    path: z.string(),
    confirm: z.literal(true).describe("삭제를 확인합니다. 반드시 true여야 합니다."),
  },
  async (params) => {
    // 추가 안전장치: 보호된 파일 목록
    const protectedFiles = [".env", "config.json", "database.sqlite"];
    const filename = path.basename(params.path);
 
    if (protectedFiles.includes(filename)) {
      return {
        content: [{
          type: "text" as const,
          text: "보호된 파일은 삭제할 수 없습니다: " + filename,
        }],
        isError: true,
      };
    }
 
    await fs.promises.unlink(sanitizePath(WORKSPACE, params.path));
    return {
      content: [{ type: "text" as const, text: "파일이 삭제되었습니다." }],
    };
  }
);

데이터 무결성 검증: 도구의 출력이 다시 LLM에 전달되므로, 출력에 악의적인 지시가 포함되지 않도록 합니다.

출력 정화
typescript
function sanitizeOutput(text: string): string {
  // LLM에 대한 지시로 해석될 수 있는 패턴 제거
  return text
    .replace(/\[SYSTEM\].*?\[\/SYSTEM\]/gi, "[내용 제거됨]")
    .replace(/\[INST\].*?\[\/INST\]/gi, "[내용 제거됨]");
}

요청 속도 제한: 비정상적으로 빈번한 도구 호출을 차단합니다.

속도 제한
typescript
class RateLimiter {
  private counts: Map<string, { count: number; resetAt: number }> = new Map();
 
  check(userId: string, limit: number, windowMs: number): boolean {
    const now = Date.now();
    const entry = this.counts.get(userId);
 
    if (!entry || entry.resetAt < now) {
      this.counts.set(userId, { count: 1, resetAt: now + windowMs });
      return true;
    }
 
    if (entry.count >= limit) {
      return false;
    }
 
    entry.count += 1;
    return true;
  }
}
 
const rateLimiter = new RateLimiter();
 
// 도구 핸들러 내에서
if (!rateLimiter.check(userId, 60, 60000)) {
  return {
    content: [{ type: "text" as const, text: "요청이 너무 많습니다. 잠시 후 다시 시도하세요." }],
    isError: true,
  };
}

SSRF 방지

서버 측 요청 위조(Server-Side Request Forgery, SSRF)는 공격자가 MCP 서버를 이용하여 내부 네트워크의 리소스에 접근하는 공격입니다.

SSRF 방지
typescript
import { URL } from "url";
import dns from "dns/promises";
 
async function validateUrl(urlString: string): Promise<boolean> {
  const url = new URL(urlString);
 
  // 1. 허용된 프로토콜만 허용
  if (!["http:", "https:"].includes(url.protocol)) {
    return false;
  }
 
  // 2. 내부 IP 대역 차단
  const addresses = await dns.resolve4(url.hostname);
  for (const addr of addresses) {
    if (
      addr.startsWith("10.") ||
      addr.startsWith("172.16.") ||
      addr.startsWith("192.168.") ||
      addr.startsWith("127.") ||
      addr === "0.0.0.0" ||
      addr.startsWith("169.254.")  // 클라우드 메타데이터
    ) {
      return false;
    }
  }
 
  // 3. 허용된 도메인 화이트리스트
  const allowedDomains = ["api.github.com", "api.example.com"];
  if (!allowedDomains.includes(url.hostname)) {
    return false;
  }
 
  return true;
}
Info

SSRF 공격은 클라우드 환경에서 특히 위험합니다. AWS의 메타데이터 엔드포인트(169.254.169.254)에 접근하면 인스턴스의 IAM 역할 자격 증명을 탈취할 수 있습니다. MCP 서버가 외부 URL에 요청을 보내는 경우, 반드시 내부 IP와 메타데이터 엔드포인트를 차단해야 합니다.

로깅과 감사

보안 사고를 탐지하고 사후 분석을 위해, MCP 서버의 모든 활동을 기록해야 합니다.

감사 로깅
typescript
interface AuditLog {
  timestamp: string;
  userId: string;
  action: string;
  toolName: string;
  params: Record<string, unknown>;
  result: "success" | "error" | "denied";
  duration: number;
  ip?: string;
}
 
function logAudit(entry: AuditLog) {
  // 민감한 정보는 마스킹
  const sanitized = {
    ...entry,
    params: maskSensitiveFields(entry.params),
  };
 
  console.error(JSON.stringify(sanitized));
  // 실제 구현에서는 외부 로깅 시스템에 전송
}
 
function maskSensitiveFields(
  params: Record<string, unknown>
): Record<string, unknown> {
  const sensitiveKeys = ["password", "token", "secret", "key", "credential"];
  const masked = { ...params };
 
  for (const key of Object.keys(masked)) {
    if (sensitiveKeys.some((s) => key.toLowerCase().includes(s))) {
      masked[key] = "[MASKED]";
    }
  }
 
  return masked;
}

보안 체크리스트

프로덕션 MCP 서버를 배포하기 전에 확인해야 할 항목을 정리합니다.

인증/인가

  • 모든 원격 엔드포인트에 인증이 적용되어 있는가
  • 토큰의 audience를 검증하는가
  • 도구별 세분화된 권한 제어가 적용되어 있는가
  • 세션 ID가 암호학적으로 안전한 방식으로 생성되는가

입력 유효성

  • 모든 도구 입력에 대한 유효성 검사가 있는가
  • SQL 쿼리가 매개변수화되어 있는가
  • 파일 경로가 정규화되고 범위가 제한되어 있는가
  • 외부 명령 실행 시 execFile을 사용하는가

네트워크 보안

  • SSRF 방지 조치가 적용되어 있는가
  • HTTPS만 사용하는가 (Streamable HTTP)
  • 내부 네트워크 접근이 차단되어 있는가

운영 보안

  • 모든 도구 호출에 대한 감사 로그가 기록되는가
  • 속도 제한이 적용되어 있는가
  • 민감한 정보가 로그에 마스킹되는가
  • 에러 메시지에 내부 정보가 노출되지 않는가

정리

이 장에서는 MCP 서버의 보안 모범 사례를 다루었습니다.

  • 인증: OAuth 2.1 기반 인증과 토큰 검증, 안전한 세션 관리
  • 인가: 도구별 세분화된 권한 제어와 스코프 설계
  • 입력 검증: 명령 주입, SQL 주입, 경로 탈출 방지
  • 프롬프트 주입 방어: 도구 수준 안전장치, 출력 정화, 속도 제한
  • SSRF 방지: 내부 IP 차단, 도메인 화이트리스트
  • 감사 로깅: 모든 활동 기록과 민감 정보 마스킹

다음 장 미리보기

10장에서는 지금까지 학습한 모든 내용을 종합하여 풀스택 MCP 시스템을 구축하는 실전 프로젝트를 진행합니다. 여러 MCP 서버를 조합하고, 클라이언트 애플리케이션을 구축하며, 보안과 모니터링을 적용하는 전체 과정을 단계별로 다루겠습니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#mcp#typescript#python

관련 글

AI / ML

10장: 실전 프로젝트 - 풀스택 MCP 시스템 구축

여러 MCP 서버를 조합하고 클라이언트 애플리케이션을 구축하여, 프로덕션 수준의 풀스택 MCP 시스템을 완성하는 실전 프로젝트입니다.

2026년 2월 8일·22분
AI / ML

8장: 기존 시스템과 MCP 연동하기

데이터베이스, REST API, 레거시 시스템을 MCP 서버로 래핑하여 AI 모델이 접근할 수 있도록 만드는 실전 패턴을 다룹니다.

2026년 2월 4일·18분
AI / ML

7장: MCP 클라이언트 구현하기

MCP 클라이언트를 직접 구현하여 서버에 연결하고, LLM과 통합하여 도구 호출 파이프라인을 완성하는 방법을 다룹니다.

2026년 2월 2일·15분
이전 글8장: 기존 시스템과 MCP 연동하기
다음 글10장: 실전 프로젝트 - 풀스택 MCP 시스템 구축

댓글

목차

약 20분 남음
  • MCP 보안의 중요성
  • 인증(Authentication)
    • OAuth 2.1 기반 인증
    • 토큰 검증
    • 세션 관리
  • 인가(Authorization)
    • 도구별 권한 제어
    • 세분화된 스코프 설계
  • 입력 유효성 검사
    • 명령 주입 방지
    • SQL 주입 방지
    • 경로 탈출(Path Traversal) 방지
  • 프롬프트 주입 방어
    • 위험 시나리오
    • 방어 전략
  • SSRF 방지
  • 로깅과 감사
  • 보안 체크리스트
    • 인증/인가
    • 입력 유효성
    • 네트워크 보안
    • 운영 보안
  • 정리
  • 다음 장 미리보기