AI 에이전트가 문서를 처리할 때 파일을 외부 서비스로 보내지 않는 구조는 매력적입니다. Rust로 쓴 PDF 도구 PaperPilot은 CLI, MCP 서버, 브라우저 WASM, 데스크톱 앱, localhost:7823 REST API가 같은 엔진을 쓴다고 설명합니다.

그런데 "로컬에서 돈다"와 "로컬 API가 안전하다"는 다른 문장입니다. Hacker News 글에서 한 댓글이 이 프로젝트의 로컬 API가 모든 웹사이트의 요청을 허용한다고 지적했고, 저는 그 주장을 현재 소스에 대조해 봤습니다. 먼저 한계를 밝힙니다. 저는 PaperPilot을 설치하거나 실행하지 않았고, 소스는 GitHub 웹에서 읽었습니다.

확인된 프로젝트 정보

README 기준입니다. 독립 검증은 아닙니다.

항목 내용
저장소 KrushnaVardhanReddy/PaperPilot (원본). hobostay/PaperPilot은 이 저장소의 포크
라이선스 Community Edition은 Apache 2.0, 엔터프라이즈 기능은 별도
표면 MCP 서버, CLI, 브라우저 WASM, Tauri 데스크톱, localhost:7823 REST API
기능(v0.6) 병합·분리·회전, 압축, 구조화 텍스트 추출, OCR(rten), 워터마크, PII 스캔, Excel 변환 등
프라이버시 주장 파일이 기기를 벗어나지 않고, "클라우드 호출 없음", 브라우저판은 메모리에서 처리
규모 별 674개, main 브랜치 커밋 1,648개(저장소 페이지 기준)

릴리스 날짜와 버전은 저장소 페이지에서 확인되지 않았습니다. 2차 출처에는 v0.6.1이 10월 26일에 나왔다는 언급이 있으나 직접 확인하지 못했습니다. 브리핑의 "최초 공개일 미확인"과 같은 상태입니다.

한 가지는 눈에 띕니다. README의 웹 임베드 예시는 cdn.usepaperpilot.com에서 스크립트를 불러옵니다. 이는 네트워크 요청이므로 "클라우드 호출 없음"은 로컬 도구에 대한 설명으로 읽는 것이 맞고, 임베드까지 포함한 말은 아닙니다. 어느 쪽이든 README가 권하는 대로 브라우저 Network 탭에서 직접 확인하는 것이 가장 확실합니다.

HN 지적을 소스와 대조하기

HN 댓글(작성자 devy)은 paperpilot-gateway/src/server.rs:95-98이 인증 없이 모든 출처의 요청을 허용한다고 썼습니다. 이 댓글은 제가 검증한 것이 아니라 한 사용자의 주장이며, 같은 댓글은 커밋의 약 98%가 AI 봇이라는 주장도 포함합니다. 저는 그중 서버 코드 한 곳만 확인했습니다. 제가 가져온 main의 server.rs는 다음과 같았습니다.

// Restrict CORS: Allow only trusted local origins (Tauri desktop client, Vite dev server, localhost)
let cors = CorsLayer::new()
    .allow_origin(tower_http::cors::AllowOrigin::predicate(|origin, _| {
        let s = origin.to_str().unwrap_or("");
        s.starts_with("tauri://")
            || s.starts_with("http://localhost")
            || s.starts_with("http://127.0.0.1")
            || s.starts_with("https://localhost")
            || s.starts_with("https://127.0.0.1")
    }))
    .allow_methods(Any)
    .allow_headers(Any);

결과는 이렇습니다.

  • "모든 출처를 허용한다"는 지적은 제가 본 버전과 맞지 않습니다. 코드는 특정 출처만 통과시키려는 의도로 보입니다. 지적 이후 수정됐을 가능성이 있지만, 커밋 이력은 확인하지 못했습니다.
  • 대신 접두사(starts_with) 검사에는 허점이 있습니다. http://localhost.evil.example도 통과합니다.
  • 이 파일에서는 인증 미들웨어가 보이지 않았습니다. 다른 파일에 있을 수 있어 "없다"고 단정하지는 않습니다.
  • 바인드 주소는 이 파일에 고정되어 있지 않고 호출 측이 정합니다. 127.0.0.1인지 0.0.0.0인지는 확인하지 못했습니다.

접두사 검사가 왜 문제인가

위 판정 로직을 그대로 옮겨 직접 실행했습니다. 실제 PaperPilot 바이너리가 아니라 논리만 복제한 예제입니다.

from urllib.parse import urlparse
def prefix_predicate(s):
    return (s.startswith("tauri://") or s.startswith("http://localhost") or s.startswith("http://127.0.0.1")
            or s.startswith("https://localhost") or s.startswith("https://127.0.0.1"))
def exact_predicate(s):
    u = urlparse(s)
    return u.scheme == "tauri" or (u.scheme in ("http", "https") and u.hostname in ("localhost", "127.0.0.1"))
http://localhost:5173              prefix=True  exact-host=True
tauri://localhost                  prefix=True  exact-host=True
https://evil.example               prefix=False exact-host=False
http://localhost.evil.example      prefix=True  exact-host=False
http://127.0.0.1.evil.example      prefix=True  exact-host=False

공격자가 localhost.evil.example 같은 도메인을 가지고 있고 사용자가 그 페이지를 열면, 브라우저가 보내는 출처가 접두사 검사를 통과합니다. 그러면 그 페이지의 스크립트가 내 컴퓨터의 localhost:7823에 요청할 수 있고, 인증이 없다면 로컬 파일을 다루는 도구까지 닿을 수 있습니다. 이 마지막 연결, 즉 API가 임의 경로 파일을 처리하는지(HN 댓글의 mcp-exec 지적)는 제가 확인하지 못했으므로 위험 평가로 쓰지 않습니다. 호스트를 파싱해 정확히 비교하는 것이 일반적인 해법입니다.

로컬 도구를 에이전트에 연결하기 전 체크리스트

PaperPilot에 한정하지 않고, 로컬 서버와 MCP 도구를 쓸 때 직접 확인할 항목입니다.

  1. 로컬 REST 서버가 127.0.0.1에만 바인드되는지 확인한다(lsof -i :7823 등).
  2. 브라우저 개발자 도구의 Network 탭에서 처리 중 외부 요청이 없는지 본다. 샘플 PDF로 먼저 시험한다.
  3. 인증이 없는 로컬 HTTP API는 같은 컴퓨터의 웹페이지가 호출할 수 있다고 가정하고, CORS 허용 목록이 정확한 호스트 비교인지 읽는다.
  4. MCP 도구가 임의 파일 경로를 받는지, 허용 디렉터리를 제한하는지 확인한다.
  5. 민감한 문서는 가짜 데이터로 먼저 시험한다.

정리

PaperPilot은 하나의 엔진을 여러 표면에서 쓰는 구조 자체는 흥미롭습니다. 하지만 "모두 로컬"이라는 주장은 설계 의도일 뿐, 로컬 포트를 여는 순간 보안 경계가 새로 생깁니다. 이 글의 확인은 소스 한 파일에 한정되며, 프로젝트 전체의 안전성이나 품질에 대한 평가가 아닙니다. 독립적으로 검증되지 않은 HN 지적은 지적일 뿐이고, 그 반대 증거도 제 확인 범위 안에서만 유효합니다.

함께 읽으면 좋은 글

참고 자료