Skip to content

Security Audit

Woojin Kim edited this page Jun 20, 2026 · 2 revisions

보안 감사 결과 (Security Audit Results)

이 페이지는 pytmux 의 보안 자세를 정리한다. 두 축으로 구성된다.

  • (A) 정적 보안 감사 — 소스 정독·데이터 흐름 추적으로 위협 모델과 신뢰 경계를 분석한 결과.
  • (B) 런타임 레드팀 / 퍼징 — 실제 서버를 띄워 적대적 입력을 던지고, 신뢰 불가 터미널 출력이 유발하는 부작용(아웃바운드 연결 등)을 차단했는지 행위로 검증한 결과.

여기 기술하는 취약점 클래스는 모두 이미 수정 적용된 상태다. 공개 위키이므로 공격 재현 레시피가 아니라 무엇을 어떻게 강화했는가를 방어적 관점으로 서술한다.


1. 위협 모델과 신뢰 경계

flowchart LR
    user["사용자"]
    client["클라이언트 (Textual 앱)"]
    server["서버"]
    child["PTY 자식 셸/프로그램"]

    user -->|"키입력"| client
    client -->|"JSON/소켓"| server
    server --> child
    server -->|"screen/status (화면=셀 그리드)"| client
Loading

pytmux 는 단일 서버(데몬)–다중 클라이언트 구조이며, 전송 계층은 두 가지다.

  • Unix(macOS/Linux): AF_UNIX 소켓. 0700 디렉터리 + 0600 소켓으로 보호 — 같은 UID 만 접근.
  • Windows: 127.0.0.1 TCP 루프백. 파일 권한 같은 per-user 통제가 없어, 같은 머신의 다른 로컬 주체가 도달할 수 있다 → 애플리케이션 레벨 인증이 필수.

핵심 신뢰 경계는 소켓 하나다. 소켓 너머에 붙은 클라이언트는 곧 사용자 본인으로 신뢰된다. 따라서 "조작된 클라이언트"는 위협이 아니라, 무인가 주체가 클라이언트가 되는 것이 위협이다.

공격자 유형

  1. 같은 머신의 다른 로컬 사용자 — 멀티유저/공용 호스트. 세션 탈취·입력 주입·화면 열람·셸 실행이 목표.
  2. 같은 사용자의 악성 로컬 프로세스 — 어차피 같은 UID 권한이라 IPC 통제로 막을 수 없음(범위 밖).
  3. 가짜 서버 / 중간자(MITM) — 피해자 클라이언트가 공격자 엔드포인트에 attach 하도록 유도. 키입력·화면 가로채기/위조.
  4. 패널 안 신뢰 불가 프로그램 출력curl 한 페이지, 악성 로그, 손상된 원격 셸 출력 등이 ANSI/OSC/DCS 제어 시퀀스를 뱉는 경우. 무인가 IPC 접근이 전혀 필요 없는 가장 넓은 표면이다.
  5. 원격(ssh) 페더레이션 — pytmux 자체는 네트워크 서비스가 아니다. 원격 노출은 사용자가 직접 소켓을 포워딩할 때로 한정되며, ssh 중첩 기동은 거부된다.

페더레이션/ssh 신뢰 모델

원격 페더레이션은 ssh 위에서만 동작하고, 원격 와이어도 로컬과 동일한 토큰 + peer-UID 인증을 거친다. 릴레이는 허용 액션 화이트리스트로 게이트되어 셸 스폰(popup/pipe/run-shell) 계열은 원격으로 중계되지 않으며, 경계 횡단 액션은 거부된다. ssh 인자 인젝션은 선행 -/공백 거부, -- 인자 종결, BatchMode=yes 로 차단한다.


2. (A) 정적 보안 감사

2.1 와이어/직렬화 — 견고함 확인

  • 와이어 프로토콜은 JSON 전용(json.loads). pickle/eval/exec/os.system/shell=True 전무(전수 grep).
  • 프레임은 4바이트 길이 프리픽스 + 본문, 최대 프레임 크기 상한(MAX_FRAME)으로 길이 폭탄 OOM 차단.
  • 모든 subprocess 는 argv 리스트(셸 메타문자 추가 주입 없음). 클립보드/버전/usageprobe 등 동일.
  • 가짜 서버 → 클라이언트 코드 실행 경로 없음: 클라이언트의 명령 해석기는 로컬 설정 파일과 사용자 프롬프트 입력으로만 구동되고, 어떤 서버 메시지도 그 경로로 흐르지 않는다. 가짜 서버의 실제 영향은 키입력 가로채기와 화면/상태 위조(피싱) 로 한정된다.
  • 패널 프로그램 출력은 서버에서 셀 그리드로 파싱되고 클라이언트는 셀(문자+스타일)만 받아 재렌더한다. raw 이스케이프가 호스트 터미널로 새는 passthrough 구간이 없다.

2.2 발견 카테고리와 적용된 하드닝

전송 계층 도달성에만 의존하던 접근 통제를 애플리케이션 레벨 인증·검증으로 보강했다.

카테고리 클래스 (CWE 유형) 적용된 완화
IPC 무인증 (Windows TCP 루프백) 인증 누락 / 무인가 접근 (CWE-306) 서버가 listen 전에 랜덤 토큰을 0600 파일에 게시 → 클라가 첫 메시지에 실어 보내고 서버가 상수시간 비교(hmac.compare_digest)로 검증. 누락/불일치는 즉시 거절. 같은 UID 만 토큰을 읽을 수 있어 무인가 루프백 접속이 차단된다
피어 신원 미검증 (Unix) 누락된 권한 검증 (CWE-862) Unix 소켓 상대 UID(SO_PEERCRED/LOCAL_PEERCRED)를 읽어 서버 UID 와 다르면 거절. 토큰 검증 위의 한 겹 더
상태 디렉터리 선점 (예측 가능 경로) 신뢰 불가 검색 경로 / 심링크 추종 (CWE-426/59) 상태 디렉터리를 쓰기 전에 lstat 으로 심링크 아님 + 현재 UID 소유를 검증해 어긋나면 fail-closed. systemd 보장 런타임 디렉터리 우선 사용
캡처 파일 정보 노출 부적절한 권한으로 민감 정보 노출 (CWE-732/200) 캡처 디렉터리 0700, 파일은 생성 시점부터 0600. 추가로 캡처 기능은 별도 플러그인으로 분리되며 기본값 OFF 로 전환(새 클론은 raw PTY 캡처를 자동으로 켜지 않음). 디렉터리를 지우면 기능 자체가 사라짐
영속 파일 권한 (심층 방어) 부적절한 기본 권한 (CWE-276) 화면 스냅샷/세션 상태/옵션/레이아웃 영속 파일을 생성 시점부터 0600 으로 기록(umask 로 잠깐 넓게 열리는 창 제거)
입력 검증 빈약 → DoS 자원 소모 / 부적절한 입력 검증 (CWE-400/20) 화면 치수(cols/rows) 상·하한 클램프, base64 디코드 가드, 첫 프레임 타입 가드
셸 실행 기능 (설계상 의도된 기능 — 취약점 아님) popup/pipe/run-shell 류는 인가된 사용자 권한 내의 의도된 기능. 별도 조치 대신 위의 접근 통제(토큰·peer-UID)로 보호

2.3 신뢰 불가 패널 출력 — VT 파서 하드닝

위협 모델 #4(신뢰 불가 출력을 보기만 함)에 해당하는 파서 결함 두 클래스를 잡았다.

  • OSC 본문 글자단위 누적 O(n²) — 긴 OSC 시퀀스를 글자마다 버퍼 전체 복사로 누적해, 패널에서 도는 긴 OSC 출력 하나가 이벤트 루프를 장시간 블록(모든 세션 동결)할 수 있었다. 종결자까지 한 번에 스캔(C 레벨 O(n)) + 본문 길이 캡(4096B)으로 선형화. (CWE-407/400)
  • 과다 파라미터 CSI → 파서 크래시 — 종결자보다 파라미터가 많은 CSI 시퀀스가 TypeError 로 패널 feed 태스크를 죽일 수 있었다. 여분 파라미터를 떼며 arity 가 맞을 때까지 재시도하도록 고쳐 신뢰 불가 출력이 파서를 못 죽이게 했다. (CWE-20/248)

2.4 아웃오브프로세스 pty-host 채널 (Windows 세션유지 재시작)

장수명 pty-host 프로세스가 별도 IPC 채널을 추가하면서, 메인 채널의 토큰/peer-UID 인증을 재사용하지 않는 인증 누락 회귀(CWE-306)가 있었다. 메인 채널과 동형 인증(host 가 listen 전 고유 토큰 게시 → 연결측 검증, Unix peer-UID 게이트, POSIX 소켓 0600)을 배선해 봉합했다.

2.5 부수 표면

  • IME 상태 소켓(ssh -R 역터널) — world-connectable·예측 가능 경로 선점 가능 → umask+chmod 0600 로 좁히고, stale 제거 전 소유권/타입 검증으로 squat 차단. 소비측 라인 버퍼에 길이 상한(메모리 DoS 차단).
  • 디렉터리 점프 플러그인 Windows 인용 — 따옴표/개행을 중화해 명령 분리·주입을 원천 차단(심층 방어).

2.6 공개 저장소 위생 / 의존성

  • 공개 트리·전체 git 히스토리 시크릿 스캔 결과 API 키·토큰·개인키·자격 증명 0건. 토큰/소켓/포트/DB 파일 커밋 이력 0건. 민감 경로는 .gitignore 로 차단.
  • 의존성은 알려진 CVE 없음(파서 핵심 라이브러리 포함). 권위 결과는 일회용 환경에서 pip-audit 실행 권장. 설치 경로 깨끗(curl|bash/원격 fetch 없음).

Windows 토큰 기밀 경계 메모: Windows 에서 0600 모드 비트는 무효이고, 토큰 기밀은 per-user 영역의 상속 ACL 에서 온다. 비관리자 로컬 사용자는 실증적으로 차단되나, 로컬 Administrator 는 토큰을 읽을 수 있다(POSIX root 예외와 유사하되 더 넓은 경계). 관리자-공유 머신이 위협이면 OS 레벨 계정 격리로 다룰 것.


3. (B) 런타임 레드팀 / 퍼징

정적 검토가 "그럴 것"이라 추론한 불변식을, 실제 서버를 띄워 코드 실행으로 확증했다.

3.1 Tier 1 — 인하네스 적대 테스트

진짜 서버를 띄우고 프로토콜을 흉내 낸 적대 클라이언트를 raw 소켓으로 붙여 검증한다.

  • 인증 우회: 무토큰/틀린 토큰 hello·control → 거절 + 서버 생존·세션 불변. 올바른 토큰만 채택.
  • 프레임 퍼징: 상한 초과 길이프리픽스 → OOM 시도 없이 드롭. 비-JSON·비-dict 첫 프레임 → 깨끗이 끊김.
  • 치수 클램프: 거대 cols/rows hello → 라이브 값이 상한 이내로 잘림.
  • 라이브 권한: 부팅된 서버의 토큰 파일·소켓이 0600 임을 실측.

런타임 퍼징이 정적 검토가 놓친 타입 가정 결함들을 추가로 드러내 모두 수정했다 — 비-dict 첫 프레임, 과다 파라미터 CSI, post-auth control 의 비-str 인자.

3.2 Tier 2 — 커버리지 가이드 퍼징

신뢰 불가 바이트를 직접 받는 경계 파서 4종(와이어 JSON·pty-host 프레임·치수 가드·VT 파서)을 퍼징한다. 불변식 = 어떤 바이트에도 ① 예외 없음 ② 출력 한계. 결정론적 baseline(신규 의존성 0, 매 CI)과 야간 커버리지 가이드 퍼저로 이중 운영하며, CI 에서 공급망 스캔과 함께 게이트한다.

3.3 Tier 3 — 라이브 레드팀 + 자원 모니터

실행 중인 서버를 밖에서 두드리며 메모리·fd(또는 Windows 핸들)를 표본한다. 순차 적대 배터리에 더해 동시 폭주(웨이브당 다수 연결을 진짜 동시에 열어 accept 루프·연결당 태스크·fd 천장 압박), 슬로로리스 내성(불완전 프레임으로 연결을 잡아둔 채 정상 트래픽 응답성 측정), post-auth 퍼징 (유효 토큰 통과 후 명령 핸들러에 악성 프레임)까지 던진다. 단언: 서버 생존·메모리 선형 상한·fd 누수 없음·무인가 수용 0. macOS/Windows 양 OS 박스에서 실측해 OS별 표면(ACL·TCP·핸들)을 못박았다.


4. (B 심층) 위조 패널 출력 → 임의 아웃바운드 차단

런타임 레드팀의 핵심 성과로, 신뢰 불가 터미널 출력이 제어 시퀀스를 위조해 데몬의 부작용을 유발하던 취약점 클래스를 잡고 차단한 작업을 분리 기록한다.

4.1 공격 클래스

pytmux 에는 ssh 로 다른 머신에 중첩 접속하면 바깥 서버가 이를 감지해 자동으로 원격 attach 로 승격하는 편의 기능이 있다(NEST). 신호는 패널 출력 스트림에 끼워 보내는 DCS(Device Control String) 제어 시퀀스다 — 하나는 ssh 목적지를 기록하고, 하나는 승격을 요청한다.

문제는 출처(provenance) 검증 부재(CWE-345: 데이터 진위 검증 불충분)였다. 터미널 출력 바이트는 누구나 만들 수 있다. 패널에서 도는 임의 출력(악성 로그를 cat, 손상된 원격 셸, 악성 프로그램 stdout)에 그 DCS 패턴만 들어 있으면, 서버가 이를 신뢰 가능한 래퍼 기록처럼 받아들였다.

연쇄적으로 (a) "안전한 입력"으로 간주되던 목적지 자체가 같은 신뢰 불가 소스에서 설정되고, (b) 호스트 일치 가드는 공격자가 양쪽 신호를 모두 emit 하면 자명하게 통과하며, (c) 목적지가 로컬 엔드포인트 형태면 보안 가드를 우회하는 직결 분기로 빠졌다.

4.2 영향

  • SSRF / 임의 아웃바운드 — 피해자가 신뢰 불가 출력을 보기만 해도 데몬이 공격자 지정 목적지로 실제 아웃바운드 연결을 시도. egress 정책으로만 가려졌던 호스트 도달·데이터 유출 채널. (CWE-918)
  • 로컬 측면 이동 — 목적지가 로컬 소켓 경로면 임의 유닉스 도메인 소켓 연결 시도.
  • 키입력 가로채기(MITM/피싱) — 승격된 탭으로 자동 전환되는 순간 키입력이 공격자 측으로 흐를 수 있는 창.

4.3 퍼징 방법과 왜 처음엔 못 잡았나

이 결함은 크래시가 아니라 파서가 완벽히 정상 파싱하는 유효 입력이었다. 기존 퍼저 오라클은 "예외 없음·출력 bounded"였고, 버그는 파싱 후 부작용에 있었으므로 오라클이 그것을 보지 못했다. 기존 런타임 적대 테스트는 IPC 소켓(인증 경계)만 쳤고, 적대적 패널 출력을 먹이고 서버 행위에 보안 속성을 단언하는 테스트가 0건이었다. 결국 provenance 가정을 의심하며 데이터 흐름을 역추적한 코드 리뷰가 잡았다.

교훈: 퍼징은 크래시를 찾지 "잘못된 신뢰"를 못 찾는다. "이 기능이 이 입력을 신뢰하면 안 된다"는 결함은 신뢰 경계를 검사 가능한 속성으로 먼저 명문화해야 잡힌다.

4.4 적용된 완화 (수정 완료)

  • (핵심) 신뢰 불가 출처에서 설정된 목적지는 로컬 엔드포인트 직결을 금지하고 항상 ssh 호스트로만 해석해 보안 가드(선행 -/공백 거부, 허용 호스트 allowlist) 경유를 강제. 정상 ssh 중첩 승격은 보존.
  • 부작용 오라클 레드팀 — 패널 출력을 먹인 뒤 소켓 연결·subprocess 호출을 가로채 "허용 endpoint 외 연결 0건"을 행위로 단언하는 회귀를 신설. 단일 feed 가 아니라 위조 토큰 DEST→REQ 시퀀스를 분할·노이즈·순서 뒤섞기로 생성하는 결정론 문법 퍼저(신규 의존성 0, 매 CI)도 추가.
  • DCS 정규식 길이 상한으로 ReDoS 차단.

설계 교훈: 첫 퍼저는 정상 래퍼가 의도대로 자기 ssh 호스트로 attach 하는 케이스를 egress 로 오탐했다. 위협 모델 #4 의 공격자는 서버 비밀 provenance 토큰을 만들 수 없으므로, 현실 적대 입력 공간은 위조 토큰뿐이다. 오라클을 그 입력 공간으로 좁혀 거짓 양성을 제거했다. → 레드팀 오라클은 '공격자가 실제로 만들 수 있는 입력 공간'으로 좁혀야 한다.


5. 현재 보안 자세 (요약)

  • 와이어/직렬화/subprocess 구성은 견고(JSON 전용·길이 상한·argv 기반, 위험 함수 전무).
  • 접근 통제는 전송 도달성 의존에서 토큰 + peer-UID 애플리케이션 인증으로 보강 — 전 플랫폼 적용.
  • 신뢰 불가 패널 출력(위협 모델 #4)이 유발하던 결함(파서 O(n²)·CSI 크래시·위조 DCS 아웃바운드) 을 차단. 특히 위조 제어 시퀀스의 부작용을 행위 오라클로 회귀 고정.
  • 정적 검토를 Tier 1~3 런타임 레드팀/퍼징으로 확장해 양 OS 박스에서 실측. 무인가 수용 0·자원 누수 없음·서버 생존을 verdict 로 게이트하고 CI 에 편입.
  • 위 발견은 모두 수정 적용 + 회귀 테스트 보유.

관련 문서

Code-Review · Project-Overview

Clone this wiki locally