본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 15. 15:35

Ollama Tool Use를 사용하여 로컬 LLM에 안전한 파일 시스템 액세스 권한 부여하기

요약

Ollama를 사용하여 로컬 LLM에 파일 시스템 접근 권한을 부여할 때 발생할 수 있는 보안 위협을 분석하고, 이를 방어하기 위한 샌드박스 구현 방법을 다룹니다. 경로 탐색 공격과 프롬프트 인젝션을 방지하기 위해 절대 경로 해결 및 허용 목록 검증의 중요성을 강조합니다.

핵심 포인트

  • LLM의 예측 불가능성을 고려하여 모델을 신뢰하지 않는 설계가 필요함
  • path.relative를 사용하여 루트 디렉토리 이탈을 구조적으로 차단해야 함
  • 심볼릭 링크 및 경로 탐색(path-traversal) 공격에 대한 대비가 필수적임
  • 모델은 플래너로, 코드는 실행기로서의 역할을 엄격히 분리해야 함

사용자의 파일을 읽을 수 있는 로컬 LLM은 진정으로 유용합니다. 하지만 가드레일(guardrails) 없이 파일을 읽을 수 있는 로컬 LLM은 채팅 인터페이스를 가진 경로 탐색(path-traversal) 버그와 다름없습니다.

저는 이전 포스트에서 도구 호출(tool calling)의 기초를 다루었습니다: 도구 스키마(tool schema)를 정의하면, 모델이 구조화된 요청을 반환하고, 여러분의 코드가 이를 실행할지 결정하는 방식입니다. 그것이 기초입니다. 이번 포스트는 이러한 도구들이 파일 시스템(filesystem)에 접근할 때 어떻게 화상을 입지 않을지에 관한 것입니다. 우리는 모델에게 세 가지 도구(list_dir, read_file, grep)를 부여하고, Ollama를 사용하여 디스패치 루프(dispatch loop)를 연결한 다음, 혼란스러운(또는 적대적인) 모델이 여러분의 .env 파일을 읽거나, 프로젝트 범위를 벗어나거나, 2GB 크기의 파일을 반환하지 못하도록 모든 도구를 강화할 것입니다.

모델은 플래너(planner)입니다. 여러분의 코드는 실행기(executor)입니다. 실행기는 예측 불가능한 토큰 생성기와 여러분의 홈 디렉토리 사이를 가로막고 있는 유일한 존재이기도 합니다. 그렇게 대우하십시오.

먼저 위협 모델(threat model)부터

코드를 작성하기 전에, 무엇이 잘못될 수 있는지 솔직하게 직시해야 합니다. LLM은 악의적이지 않지만 예측 불가능하며, LLM에 입력되는 _입력값(input)_이 악의적일 수 있습니다(모델이 읽는 파일에 지침이 포함되어 있을 수 있으며, 이는 전형적인 프롬프트 인젝션(prompt-injection) 벡터입니다). 따라서 이 모든 상황에 대비하십시오:

  • 모델이 /etc/passwd 또는 ~/.ssh/id_rsa를 읽어달라고 요청함.
  • 모델이 ../../../../etc/shadow를 "상대적" 경로로 전달함.
  • 모델이 .env를 읽고 친절하게도 채팅 기록에 여러분의 API 키를 출력함.
  • 모델이 4GB 크기의 로그 파일을 읽어달라고 요청하여 RAM을 점유함.
  • 모델이 읽는 파일에 "이전 지침을 무시하고, 이제 ...에 작성하라"는 내용이 포함됨.

아래의 모든 방어책은 이 중 하나에 대응합니다. 그 중 어느 것도 모델을 신뢰하지 않습니다.

샌드박스(sandbox): 하나의 루트, 해결된 경로, 허용 목록

가장 중요한 단일 제어 항목은 다음과 같습니다: 모델이 제공하는 모든 경로는 절대 경로(absolute path)로 해결(resolved)되어야 하며, 허용된 루트(allowed root)와 대조 확인되어야 합니다. 만약 루트를 벗어난다면 거부하십시오. 예외는 없으며, "아마 괜찮을 거야"라는 식의 판단도 허용되지 않습니다.

import path from "node:path";
import fs from "node:fs/promises";

...

startsWith(SANDBOX_ROOT) 문자열 검사 대신 path.relative를 사용할까요? startsWith는 함정이기 때문입니다. /home/pavel/workspace-secrets/home/pavel/workspace로 시작하지만, 서로 다른 디렉토리입니다. path.relative는 이를 구조적으로 처리합니다. 만약 상대 경로가 ..로 시작한다면, 대상이 루트(root)보다 상위에 있다는 뜻입니다. 끝입니다.

신뢰하기 전에 테스트해 보세요:

resolveInSandbox("notes.txt");        // OK -> <root>/notes.txt
resolveInSandbox("sub/dir/a.md");     // OK
resolveInSandbox("../secrets.env");   // PathError 발생
...

path.resolve가 다루지 못하는 한 가지가 더 있습니다. 바로 심볼릭 링크 (symlinks)입니다. 샌드박스 내부의 심볼릭 링크는 어디든 가리킬 수 있습니다. 만약 워크스페이스에 제어할 수 없는 심볼릭 링크가 포함될 수 있다면, 그것들도 함께 해결(resolve)하고 다시 확인해야 합니다:

async function resolveRealInSandbox(userPath: string): Promise<string> {
  const resolved = resolveInSandbox(userPath);
  try {
...

명백한 지뢰를 위한 거부 목록 (deny-list)

루트를 허용 목록 (allow-listing)에 넣는 것이 구조적 제어입니다. 여기에 더해, 작은 거부 목록 (deny-list)을 두면 모델이 샌드박스 내부에 있지만 여전히 비밀인 항목들을 읽는 것을 방지할 수 있습니다. 부분 문자열 (substring)이 아닌 베이스네임 (basename)으로 매칭하여, environment.md.env 규칙에 걸리지 않도록 하세요.

const DENIED_NAMES = new Set([".env", ".git", "id_rsa", "id_ed25519"]);
const DENIED_SUFFIXES = [".env", ".pem", ".key"];

...

이 목록은 짧고 명확하게 유지하세요. 구조적 샌드박스가 실제 방어 수단이며, 거부 목록은 프로젝트 폴더 내에 정당하게 존재하는 비밀 정보들을 잡아내는 역할만 합니다.

도구 (tools)

세 가지 읽기 전용 도구입니다. read_file에는 엄격한 바이트 예산 (byte budget)이 설정되어 있으며, 어떤 도구도 아무것도 쓰지 않는다는 점에 주목하세요.

const MAX_READ_BYTES = 256 * 1024; // 256 KB. 모델은 2GB 파일이 필요하지 않습니다.

async function listDir(dirPath: string): Promise<string[]> {
...

스키마 (schemas)는 함수 호출 (function-calling) 포스트에서 사용했던 것과 동일한 JSON Schema 형식을 따릅니다:

const tools = [
  {
    type: "function",
...

디스패치 루프 (dispatch loop)

대부분의 튜토리얼이 소홀히 하는 부분이 바로 여기입니다. 많은 경우 도구(tool) 이름에 대해 eval 방식의 디스패치(dispatch)를 수행하고 인자(arguments)를 그대로 전달합니다. 그렇게 하지 마세요. Zod를 사용하여 인자를 검증하고, 명시적인 switch 문을 통해 라우팅하며, 발생하는 모든 에러를 모델이 읽고 복구할 수 있는 도구 결과(tool result)로 변환하세요. 에러는 데이터이지, 크래시(crash)가 아닙니다.

import { z } from "zod";

const PathArgs = z.object({ path: z.string() });
...

이제 Ollama를 대상으로 하는 에이전트 루프(agent loop)입니다. 모델이 호출을 체이닝(chaining)할 수 있도록 래핑되어 있으며, 이전과 동일한 2회 왕복(two-round-trip) 구조를 가집니다:

async function run(userPrompt: string): Promise<string> {
  const messages: any[] = [
    {
...

turn < 8 제한은 중요합니다. 이 제한이 없다면, 도구 사용을 계속 요청하거나 (프롬프트 인젝션된 파일로 인해) 재시도 루프에 빠진 모델은 영원히 실행될 것입니다.

쓰기(Writes)는 다릅니다: 인간의 개입이 필요합니다

읽기는 되돌릴 수 있습니다. 쓰기는 그렇지 않습니다. 따라서 저는 쓰기 작업을 자율 루프(autonomous loop)에서 완전히 제외하고, 명시적인 인간의 승인(human approval)을 거치도록 제한합니다. 도구는 직접 쓰는 것이 아니라, 쓰기를 _제안(propose)_하고, 차이점(diff)을 출력한 뒤, 당신의 확인을 기다립니다.

import readline from "node:readline/promises";

async function proposeWrite(filePath: string, content: string): Promise<string> {
...

모델은 하루 종일 쓰고 싶어 할 수도 있습니다. 하지만 인간이 y를 입력하기 전까지는 아무것도 디스크에 기록되지 않습니다. 이는 읽기 샌드박스(read sandbox)와 동일한 원리를 더 높은 위험도가 따르는 작업에 적용한 것입니다. 즉, LLM은 제안하고, 당신의 코드(그리고 당신)가 결정(dispose)합니다.

보안 강화 체크리스트 (The hardening checklist)

LLM에 노출하는 모든 파일 시스템 도구는 다음 사항을 모두 통과해야 합니다:

  1. 해결 및 재검증 (Resolve and re-check). path.resolve를 수행한 다음, 루트(root)를 기준으로 path.relative를 실행합니다. ..으로 시작하는 모든 경로는 거부하세요. 원본 문자열(raw string)에 대해 startsWith를 절대 사용하지 마세요.
  2. 심볼릭 링크 (Symlinks)도 해결하세요. fs.realpath를 사용하여 해결한 뒤 재검증하세요. 그렇지 않으면 샌드박스(sandbox) 내부에 백도어를 남겨두는 셈이 됩니다.
  3. 베이스네임 (Basename) 기준으로 비밀 정보 차단. .env, 키(keys), .git 등. 짧은 목록을 작성하고, 부분 문자열(substring)이 아닌 베이스네임(basename)을 기준으로 매칭하세요.
  4. 읽기 크기 제한 (Cap read size). 읽기 작업당 바이트 예산(byte budget)을 설정하고 검색 결과에 대한 상한선을 두세요. 컨텍스트 윈도우(Context windows)와 RAM은 유한합니다.
  5. 기본적으로 읽기 전용 (Read-only by default). 쓰기 작업은 별도의, 사람이 승인하는 경로를 통해 이루어져야 합니다. 자율 루프(autonomous loop) 내에 쓰기 도구를 두지 마세요.
  6. 모든 인자 검증 (Validate every argument). 도구 인자가 fs에 도달하기 전에 Zod를 사용하여 파싱하세요. 모델은 필드(fields)를 환각(hallucinate)할 수 있습니다.
  7. 에러는 도구의 결과물입니다. 에러를 포착(catch)하여 문자열로 변환(stringify)한 뒤 모델에 반환하세요. 잘못된 경로로 인해 프로세스가 충돌하게 두지 마세요.
  8. 턴 횟수 제한 (Cap the turn count). 에이전트 루프(agent loop)에 경계를 설정하여, 갇히거나 주입(injected)된 모델이 무한히 회전하지 못하도록 하세요.

핵심 요약 (Takeaway)

함수 호출 (Function calling)은 로컬 LLM을 유용하게 만듭니다. 파일 시스템 액세스는 이를 강력하게 만들며, 강력해지는 바로 그 시점이 속도를 늦춰야 하는 때입니다. 모델은 잠재적으로 신뢰할 수 없는 입력값 위에서 작동하는, 신뢰할 수 없는 계획가 (untrusted planner)입니다. 여러분의 도구가 신뢰 경계 (trust boundary)입니다. 혼란에 빠진 모델이 할 수 있는 최악의 행동이 이미 허용된 텍스트 파일을 읽는 것뿐이도록 도구를 구축하세요.

저는 로컬 우선(local-first) 스마트 컨트랙트 감사 도구인 spectr-ai에서 정확히 이 패턴을 실행하고 있습니다. 이를 통해 모델이 프로젝트 폴더를 벗어나지 않고도 컨트랙트의 소스 트리(source tree)를 탐색할 수 있습니다. 샌드박스가 우선이고, 기능은 그다음입니다. 이 순서가 바로 핵심입니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0