⚠️ 판정: 기본 핵심 경로는 인증된 flow 작성 권한 사용자의 동적CodeInput검증 우회다. Agentic MCP와 공개 flow·자동 로그인 설정은 별도의 조건부 확대 체인이다.
요약
CVE-2026-9135는 IBM Langflow OSS Policies/ToolGuard의 서버 측 Python Code Injection이다.
allow_custom_components=false에서도 기존 검증은 주 코드 필드만 확인했고, guard 파일용 동적 CodeInput은 같은 검증·해시 경계에 포함되지 않았다.조작된 guard 소스는 flow에 저장된 뒤
make_toolguard_result()에서 복원되고, Guard 모드의 load_toolguards_from_memory()를 거쳐 백엔드에서 실행됐다.핵심 정보
항목 | 확인된 내용 |
CVE 상태 | PUBLISHED |
제품 | IBM Langflow OSS / upstream langflow-ai/langflow |
공식 영향 버전 | 1.0.0–1.10.0, 양 끝 포함 |
수정 버전 | 1.10.1, 태그 커밋 a66b75ac2603b26988fb6be95303fdc61f807190 |
취약점 유형 | Stored server-side Python Code Injection |
공식 CWE | CWE-94, Improper Control of Generation of Code |
CVSS | 9.9 Critical — IBM CNA 산정 |
CVSS 벡터 | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H |
기본 공격자 | 인증됐으며 flow를 생성·수정할 수 있는 사용자 |
실행 트리거 | Policies Guard 모드에서 guarded tool 호출 |
공식 workaround | 없음 |
CISA KEV | 2026-08-30 검토 기준 미등재 |
CVSS 9.9는 NVD가 독립적으로 산정한 점수가 아니라 IBM CNA가 제공한 CVSS v3.1 점수다. NVD의 자체 NIST 평가는 현재
N/A이므로 “NVD CVSS 9.9”라고 쓰지 않고 “IBM CNA CVSS 9.9”로 표기한다.영향 버전과 버전 표기 주의
IBM Security Bulletin과 CVEProject 원본 레코드는 다음 범위를 명시한다.
- 영향: Langflow OSS
1.0.0–1.10.0
- 수정: Langflow OSS
1.10.1
- NVD CPE 해석:
>=1.0.0,<1.10.1
- 벤더 조치:
1.10.1로 업그레이드
- 공식 workaround 및 mitigation:
None
CNA 설명에는 “versions up to 1.9.2 (commit
94981c...)”라는 문구가 함께 들어가지만, 이를 공식 영향 범위로 축소 해석하면 안 된다. 94981c443d4918517b9e8163d70fc598dc33a32d는 2026-05-06 작성된 Docker 베이스 이미지 갱신 커밋이다. v1.9.2 태그는 별도의 ea3eae8b9e011ff85d7f92f00cf07916dccf755e이며, 실제 보안 수정도 94981c...가 아니다. 따라서 94981c...는 CNA 문장에 포함된 발견·검증 시점의 저장소 스냅샷 식별자로만 다루고 “도입 커밋”이나 “fix commit”으로 표시하지 않는다.기본 공격 전제
확인된 핵심 경로는 다음 조건을 전제로 한다.
- 공격자가 Langflow 인스턴스에 네트워크로 접근할 수 있다.
- 공격자가 인증됐고 flow를 생성하거나 수정할 권한을 가진다.
- flow에 활성화된 Policies/ToolGuard 컴포넌트가 존재한다.
- 공격자가 해당 컴포넌트의 동적
CodeInput값을 제어할 수 있다.
- 배포는 사용자 코드 실행을 차단하려고
allow_custom_components=false를 사용한다.
- Policies가 참조하는 유효한 ToolGuard
Step_2cache/result가 정상 workflow에서 생성됐거나 보존돼 있다.
- flow가 Guard 모드에서 실행되고 guarded tool이 호출된다.
allow_custom_components=false는 핵심 정책 조건이다. true이면 ToolGuard 코드 실행이 배포 정책상 허용되지만, false인 취약 버전에서는 운영자의 코드 실행 금지 의도와 실제 경로가 불일치했다. 이 경로만으로 기본 설정의 무인증 RCE를 주장할 수는 없다.신뢰 경계와 루트 원인
취약한 신뢰 경계는 브라우저·API에서 편집 가능한 flow JSON과 백엔드 Python 실행기 사이에 있다.
사용자 편집 가능 동적 CodeInput ↓ Flow.data["nodes"][i]["data"]["node"]["template"][동적 파일명]["value"] ↓ PoliciesComponent.make_toolguard_result() ↓ ToolGuardsCodeGenerationResult의 파일 content 복원 ↓ load_toolguards_from_memory() ↓ GuardedTool 생성 및 guarded tool 호출 ↓ Langflow 백엔드 프로세스 안에서 Python 실행
make_toolguard_result()는 app_types, app_api, app_api_impl와 tool별 guard 파일을 attrs[파일명]["value"]에서 복원했다. 기존 정책은 컴포넌트의 주 코드·해시를 확인했지만 이 동적 content를 같은 검증에 묶지 않아 공격자 데이터가 실행 sink에 도달했다. 값이 Flow.data에 보존되므로 저장과 실행 시점이 분리된다.판단 변화: 정적 가설에서 최종 판정까지
이 절은 최종 결론만 나열하지 않고, 원본 조사 대화에서 어떤 의심을 세웠고 어떤 질문·실험·후속 감사 때문에 판단을 올리거나 낮췄는지를 시간순으로 기록합니다. 비공개 session 원문, 다른 finding의 세부 내용과 민감한 경로·식별자는 옮기지 않고, 공개 가능한 기술 사실과 정제된 산출물 이름만 요약합니다.
이 조사의 변화는 단순한 “계속 승격”이 아니었습니다. 핵심 ToolGuard code-injection 인과관계의 신뢰도는 단계마다 높아졌지만, cross-tenant·public·no-key 확대 범위는 후속 감사에서 더 엄격하게 조건부로 좁아졌습니다.
현재 판정: 확인된 핵심은 flow 생성·수정 권한이 있는 인증 사용자가
allow_custom_components=false 아래에서 동적 ToolGuard 필드를 주입하고, 해당 guarded tool이 실제 호출될 때 backend Python을 실행시키는 application-core 경로입니다. 실제 DB 저장·HTTP/MCP transport·자율 Agent의 tool 선택을 한 번에 통과한 E2E와 cross-tenant/public/no-key 전체 체인은 확인하지 않았습니다.변화 축 | 초기 상태 | 최종 상태 |
핵심 인과 신뢰도 | 정적 validation blind spot 후보 | validation·negative control·ToolGuard sink·Policies·Graph· Flow.data application core·fresh recreation이 일치하는 Confirmed core |
공격 범위 | 자기 flow, public/webhook, cross-tenant, no-key 가능성을 폭넓게 탐색 | 기본은 인증된 flow 작성자, public trigger는 source·handler/core 수준의 조건부 근거, cross-tenant는 helper 수준의 조건부 근거, 기본 설정 무인증 RCE는 미입증 |
단계 | 당시 판단·가정 | 전환 계기 | 바뀐 판단 | 증거 수준·남은 한계 |
1. 5월 7일 15:33 UTC · 후보 선별 | 다수의 Langflow finding 중 어떤 것이 제품의 실제 신뢰 경계를 넘는지 미정이었습니다. | 정상 custom component 실행은 설계일 수 있지만, allow_custom_components=false는 사용자 Python을 막는 명시적 경계이고 Policies의 동적 code field는 그 경계 밖에 있음을 코드에서 발견했습니다. | Policies/ToolGuard를 최우선 동적 검증 후보로 선택했습니다. | 정적 코드 가설이며 취약점 확정 전이었습니다. |
2. 15:42 · Validation blind spot | Main template["code"]만 검사한다는 사실이 실제 우회로 이어질 것으로 보았습니다. | 정상 Policies main code와 악성 동적 CodeInput을 함께 둔 payload는 통과했고, main code 자체를 바꾼 negative control은 CustomComponentValidationError로 차단됐습니다. | 검증기가 code-bearing field 전체가 아니라 main code만 신뢰한다는 blind spot을 확정했습니다. | 실제 validator·component index 확인. 저장·실행은 아직 미확인 상태였습니다. |
3. 16:03 · 실행 sink 분리 | 검증 우회가 있더라도 동적 값이 실제 Python 실행 sink까지 가는지는 별도 문제였습니다. | ToolGuard 0.2.17 in-memory runtime enter 전 marker는 없고 enter 후 marker가 생성됐습니다. | Attacker-controlled guard content가 실행 가능한 Python sink라는 점을 별도로 확정했습니다. | Langflow validation과 ToolGuard sink가 아직 하나의 실행으로 연결되지는 않았습니다. |
4. 16:04 · “아직 E2E 아님” | 두 조각이 확인되면 취약점 전체가 입증됐다고 볼 여지가 있었습니다. | 사용자가 “E2E 재현이 된 것인가”를 다시 물었고, 당시 PoC가 validator와 loader/runtime을 서로 다른 실험으로 확인했다는 한계를 재검토했습니다. | Validation bypass와 sink는 각각 확인됐지만 Langflow 안의 하나의 payload 경로로 연결되기 전에는 완전한 E2E라고 부르지 않기로 했습니다. | 사용자 질문이 다음 통합 PoC의 기준을 만든 전환점입니다. |
5. 16:16 · Policies 통합 | 유효한 cache 검사 뒤 flow template 값이 실제 runtime 구성에 사용되는지 확인해야 했습니다. | PoC가 정상 출력 형식으로 수동 구성한 유효한 benign Step_2 cache/result로 _validate_before_using_cache()를 통과시켰습니다. Disk cache는 benign으로 두고 flow template의 result.json과 guard-file 동적 값에는 같은 구조의 marker payload를 넣자, 실제 PoliciesComponent.guard_tools()와 GuardedTool.arun() 뒤 marker가 생성됐습니다. | 검사 대상인 disk cache와 실행용으로 재구성되는 node-template content가 다르며, component 내부에서 두 조각이 연결된다고 확정했습니다. | 실제 Policies component path 확인. Flow 모델·DB·network transport는 제외했습니다. |
6. 16:31 · Graph 통합 | Component 단위 성공만으로 실제 flow payload가 graph 실행에 도달한다고 단정하기에는 부족했습니다. | Graph.from_payload() validation·graph runner·Policies vertex를 통과한 뒤 guarded tool 호출에서 marker가 생성됐습니다. | 고립된 component helper가 아니라 실제 graph integration에서도 핵심 인과가 유지된다고 판단했습니다. | 반환된 guarded tool을 PoC가 직접 호출했습니다. DB·HTTP·자동 Agent 선택은 제외했습니다. |
7. 16:40–17:03 · 공격 모델 재평가 | 초기에는 “배포 환경 의존적인 Critical”이라고 넓게 표현했습니다. | 사용자가 공격 모델이 현실적인지 재질문했습니다. Langflow에서 flow 생성·저장·import·공유·실행은 정상 입력면이지만, 단일 사용자 로컬 설치에서는 작성자와 서버 운영자가 같은 신뢰 주체일 수 있음을 함께 검토했습니다. | 기본 공격자를 인증된 일반 사용자 + flow 생성·수정 권한으로 고정하고, shared·hosted·team·import-capable 환경에서는 자연스러운 경계 침해라고 판단했습니다. | 기본 설정 무인증 공격이나 모든 배포의 동일한 영향은 주장하지 않습니다. |
8. 17:10 · Flow.data application core | Graph 성공 뒤에도 동적 field가 실제 application data model과 run core에서 보존되는지 확인할 필요가 있었습니다. | Flow.data 모델이 type="code"로 표시된 동적 필드 여섯 개를 보존하고 simple_run_flow() graph construction 뒤에도 같은 guarded wrapper가 만들어지는 것을 확인했습니다. 이 중 guard 파일들은 Python이고 result.json은 metadata입니다. | 핵심 판단을 component/graph bug에서 Flow.data 모델 기반 application-core Python code injection으로 강화했습니다. | In-memory Flow와 직접 guarded-tool invocation을 사용했습니다. 8월 공개 derivative는 내부 runner를 monkeypatch해 동등한 standalone LFX runner로 위임했으며 실제 DB transaction·HTTP 요청은 포함하지 않습니다. |
9. 17:16–17:43 · 외부 trigger 탐색 | Public flow, webhook 또는 signup을 연결하면 무인증 외부 trigger로 승격할 수 있다고 가정했습니다. | 소스 추적과 handler/core 실험은 public build나 WEBHOOK_AUTH_ENABLE=false, NEW_USER_IS_ACTIVE=true 조건에서 실행 경로 일부를 보였지만, ToolGuard Python은 저장·build 시가 아니라 GuardedTool.arun()에서 실행됐습니다. LLM의 선택에 의존하지 않고 guarded-tool 호출을 보장하는 후속 경로도 확인하지 못했습니다. | 외부 trigger는 별도 설정과 후속 tool invocation이 필요한 조건부 가능성으로 낮췄습니다. | Webhook/signup helper PoC는 base CVE의 confirmed 공개 evidence에서 제외했습니다. |
10. 17:50–18:01 · Cross-tenant 확대 | “자기 flow를 자기가 실행한 것”이라는 반박을 제거하려면 다른 사용자의 flow에 payload를 심는 경로가 필요했습니다. | Agentic helper가 caller-supplied user_id를 lookup과 ownership check에 사용했고, public FlowRead.user_id에서 update helper로 이어지는 component-level data flow를 재현했습니다. | 권한 혼동 primitive는 helper 수준에서 재현했지만 실제 cross-tenant 저장 체인으로 확정하지 않았습니다. | FakeSession, 직접 helper 호출과 monkeypatch를 사용했습니다. 실제 인증 MCP transport·multi-tenant DB·network E2E는 미검증입니다. |
11. 5월 7일 18:04 관찰 → 8월 30일 재감사 · No-key 한계 | AUTO_LOGIN=true auth helper가 key 없이 superuser를 반환하는 관찰을 agentic MCP 체인과 연결할 수 있다고 보았습니다. | 후속 감사에서 global network MCP auth 관찰과 per-user agentic stdio MCP update surface 사이의 실제 bridge가 증명되지 않았음을 확인했습니다. | AUTO_LOGIN은 특정 배포의 조건부 amplifier 후보로만 남기고 confirmed chain에서 제외했습니다. | mcp_auto_login_no_key_poc.py는 public core evidence에서 제외했습니다. |
12. 5월 9–10일 · 버전 식별 교정 | Tested checkout의 package version 문자열만 보고 공식 v1.9.2 tag와 동일시할 위험이 있었습니다. | 5월 9일 triage 후속 확인에서 94981c가 v1.9.2 tag가 아니라 그 이후 main commit임을 확인했고, 5월 10일 git describe로 v1.9.2-5-g94981c443d를 고정했습니다. | 조사 기준을 공식 release tag가 아닌 exact commit과 post-release main snapshot으로 교정했습니다. | 공식 영향 범위는 이후 IBM CNA의 구조화된 1.0.0–1.10.0을 source of truth로 사용합니다. |
13. 8월 30일 · 공개화 재감사 | 원본 파일명과 당시 보고서에는 graph/API/cross-tenant 실험을 e2e로 부르는 표현이 있었습니다. | Clean 94981c...와 ToolGuard 0.2.17에서 공개용 PoC 7개를 다시 실행하고 각 stub, direct call, disabled persistence와 transport 경계를 재검토했습니다. | Core 5개는 confirmed 단계 증거로 유지하되 graph/stored는 integration·application-core로, cross-tenant 2개는 supplemental helper-level로 재분류했습니다. | Fresh 환경은 5월 환경의 byte-for-byte 복원이 아닙니다. Fixed v1.10.1 binary E2E, 실제 DB/HTTP/MCP transport, 기본 설정 무인증 RCE는 주장하지 않습니다. |
14. 공식 기록 · Fix와 범위 삼각검증 | 공식 prose만 보면 영향이 1.9.2까지이거나 최초 PR head 955c8b1가 release fix라고 오해할 수 있었습니다. | IBM CNA structured affected table, PR merge metadata와 v1.10.1 ancestry를 대조했습니다. | 공식 metadata 기준 영향은 1.0.0–1.10.0, primary merged fix는 0b14a6b, fixed release는 a66b75a로 정리했습니다. 실제 수정도 동적 field별 hash가 아니라 실행 sink 앞 fail-closed gate임을 구분했습니다. | 공식 metadata·upstream diff/ancestry 확인. Fixed binary 동적 회귀는 수행하지 않았습니다. |
1. 정적 의심에서 반증 가능한 두 실험까지
첫 정적 가설은 단순했습니다. Flow validator는 각 node의 main
template["code"]["value"]만 trusted hash와 비교하지만, Policies는 ToolGuard가 생성한 Python 파일을 다른 동적 CodeInput으로 저장합니다. 그렇다면 정상 main code를 유지하면서 동적 field만 바꾼 payload가 보안 설정을 통과할 수 있다는 가설이 생깁니다.이를 한 번의 복잡한 PoC로 바로 증명하려 하지 않고 두 주장으로 분리했습니다.
- 실제 Langflow validator가 악성 동적 field를 남긴 payload를 통과시키는가?
- ToolGuard가 같은 종류의 content를 실제 Python으로 실행하는가?
첫 실험에는 main code 변경 대조군을 넣었습니다. 동적 field만 바꾼 payload는 통과하고 main code를 바꾸면 차단됐으므로, “검증 함수가 전혀 동작하지 않는다”가 아니라 검증은 작동하지만 검사 범위가 실행 가능한 동적 field를 놓친다는 더 좁은 원인이 확인됐습니다. 두 번째 실험은 runtime enter 전후 marker 차이로 sink의 실행 시점을 분리했습니다.
그러나 이 시점에는 두 실험이 서로 독립적이었습니다. 사용자가 E2E 여부를 다시 확인했을 때 “취약 조건과 sink는 각각 확인됐지만 하나의 Langflow payload로 연결되지는 않았다”고 판단을 멈춘 것이 이후 증거 설계를 바꿨습니다.
2. 분리 증명에서 Policies·Graph·stored-flow core까지
통합 PoC는 정상 출력 형식을 모사해 유효한 benign
Step_2 cache/result를 수동 구성했습니다. Guard mode의 _verify_cached_guards()는 이 disk cache를 확인합니다. Disk cache는 benign으로 유지한 반면, flow template의 result.json과 guard-file 동적 값에는 같은 구조의 marker payload를 넣었습니다. 그 뒤 make_toolguard_result()는 실행할 파일 content를 disk와 대조하지 않고 node template의 동적 값에서 다시 채웠습니다. 유효한 cache 검사를 통과한 뒤 공격자가 바꾼 node-template content가 in-memory runtime에 들어가는 검사 대상과 실행 대상의 불일치가 실제 Policies component 안에서 확인됐습니다.그 다음
Graph.from_payload()와 graph runner를 통과시켜 이 동작이 고립된 helper에만 존재하지 않음을 확인했습니다. Flow.data application-core PoC에서는 모델이 type="code"로 표시된 동적 필드 여섯 개를 보존했습니다. 이 중 guard 파일들은 Python이고 result.json은 metadata입니다. simple_run_flow() graph construction 뒤에도 guarded wrapper가 남았습니다.이 단계에서 핵심 인과는 다음처럼 연결됐습니다.
flow-controlled dynamic CodeInput → main-code-only validation 통과 → Flow.data에 보존 → Policies가 ToolGuard result로 재구성 → GuardedTool.arun() runtime enter → backend Python 실행의 부작용으로 marker 생성
다만 공개 graph/stored PoC는 반환된 guarded tool을 직접 호출합니다. 특히 2026-08-30 stored-flow derivative는 실제
simple_run_flow()로 graph를 구성하지만 내부 run_graph_internal을 monkeypatch해 동등한 standalone LFX runner로 위임합니다. 실제 DB에 저장한 뒤 HTTP 요청과 downstream Agent의 자율 tool selection까지 한 번에 통과한 transport-level E2E는 아닙니다. 따라서 공개 문서는 이를 Graph integration과 Stored-flow application core로 부릅니다.3. 공격 모델이 현실적인지 다시 판단한 과정
초기에는 영향이 배포 모델에 따라 달라진다는 점 때문에 “배포 환경 의존적”이라는 표현을 사용했습니다. 사용자가 이 전제가 인위적인지 다시 질문하면서 제품의 정상 사용 흐름을 기준으로 재평가했습니다.
Langflow는 사용자가 flow를 만들고 JSON으로 저장·import·공유·실행하는 제품입니다. Flow payload 제어는 특수한 우회 입력이 아니라 핵심 기능입니다. 또한
allow_custom_components=false는 flow 작성자를 서버 관리자와 같은 신뢰 수준으로 보지 않으면서 Python 실행을 막기 위해 존재하는 정책입니다. 따라서 인증된 일반 사용자가 flow를 만들 수 있다는 전제는 shared·hosted·team 환경에서 자연스럽습니다. 실제 실행에는 유효한 Step_2 cache/result, Guard mode와 후속 guarded-tool invocation이 추가로 필요합니다.반대로 완전한 단일 사용자 로컬 설치에서 flow 작성자와 서버 운영자가 같은 사람이라면 경계 침해의 실질적 가치가 낮아질 수 있습니다. 이 검토를 거쳐 “모든 Langflow가 즉시 위험”이 아니라 저신뢰 flow 작성자와 backend 운영 권한이 분리된 배포에서 명시적 코드 실행 금지 경계를 깨는 취약점으로 공격 모델을 고정했습니다.
4. 확대 가능성을 실제 증거와 분리한 과정
핵심 체인이 확인된 뒤에는 public build, webhook, signup, agentic MCP와 결합해 공격 전제를 줄일 수 있는지 조사했습니다. 이 과정에서 가능성과 실제 증명을 계속 분리해야 했습니다.
- Public/webhook handler가 flow 실행을 시작할 수 있다는 사실만으로 ToolGuard Python이 실행되지는 않습니다. 실제 sink는 guarded tool invocation입니다.
WEBHOOK_AUTH_ENABLE=false와NEW_USER_IS_ACTIVE=true는 기본 핵심이 아니라 별도 배포 조건입니다.
- Downstream Agent가 tool을 선택할 것이라는 가정은 deterministic하지 않으며, 공개 재현은 그 transport-level 자동 호출을 증명하지 않았습니다.
- AUTO_LOGIN auth helper의 no-key 동작과 agentic MCP update tool 사이에는 실제 end-to-end bridge 증거가 없습니다.
따라서 webhook/signup/AUTO_LOGIN 실험은 삭제한 것이 아니라 private provenance에 보존하되, base CVE의 confirmed public set에서는 제외했습니다. “더 강해질 수 있음”과 “실제로 그 강한 체인을 끝까지 재현함”을 구분한 결정입니다.
5. Cross-tenant 주장을 다시 낮춘 이유
Agentic update helper가 호출자 제공
user_id를 그대로 lookup과 소유권 확인에 사용한다는 것은 실제 source와 helper 실행에서 확인했습니다. Public flow 응답의 user_id가 그 helper 입력으로 이어질 수 있는 data flow도 확인했습니다. 이 때문에 원본 조사 당시에는 cross-tenant planting을 강한 E2E로 표현했습니다.그러나 공개화 재감사에서는 “실제 helper가 실행됐다”와 “네트워크 공격자가 실제 인증 transport를 거쳐 타 tenant DB row를 수정했다”를 분리했습니다. PoC는
FakeSession, 직접 helper 호출과 monkeypatch를 사용했으므로 다음을 증명하지 않습니다.- 실제 authenticated agentic MCP transport 도달
- 실제 multi-tenant database transaction
- unauthenticated HTTP에서 agentic MCP로 이어지는 bridge
- 피해자 또는 automation이 이후 guarded tool을 실제로 선택·호출하는 전체 경로
이 conditional chain은 PUBLIC victim flow UUID, agentic update tool surface에 대한 별도 접근, victim flow에 이미 존재하는 Policies dynamic guard field와 이후 guarded-tool invocation을 전제로 합니다.
따라서 authorization-confusion primitive 자체는 강하게 지지되지만, 공개 판단은 Conditional helper-level chain입니다. 후속
1641b28가 caller-supplied user_id를 제거했다는 사실은 이 경계의 중요성을 뒷받침하지만, 미검증 transport를 대신 증명하지는 않습니다.6. 당시의 강한 표현을 숨기지 않고 교정한 과정
원본 파일명에는
component_e2e, graph_e2e, api_stored_flow_e2e처럼 현재 증거 경계보다 넓은 이름이 있었고, 원본 세션은 cross-tenant planting도 e2e라고 불렀습니다. 2026-08-30 재감사에서는 그 표현을 그대로 유지해 과거 확신을 정당화하지 않고 실제 호출 경계를 기준으로 다음처럼 교정했습니다.- Policies component: component integration
- Graph: graph integration, DB/HTTP 제외
- Stored flow: application run core, 실제 persistence/transport 제외
- Cross-tenant/public flow: supplemental component/helper evidence
- Webhook/signup/AUTO_LOGIN: confirmed public evidence에서 제외
같은 방식으로 tested checkout도
v1.9.2 release tag와 동일시하지 않고 v1.9.2-5-g94981c discovery snapshot으로 교정했습니다. load_toolguards_from_memory()를 즉시 실행 지점으로 부르지 않고 runtime을 구성하는 단계로, 실제 trigger를 GuardedTool.arun()으로 좁혔습니다. 후속 공식 자료에서는 영향 범위와 fix 계보도 structured CNA·merge ancestry를 기준으로 다시 고정했습니다.7. 사용자와 Codex의 대화가 검증 기준을 바꾼 지점
이 조사에서 사용자 질문은 단순 진행 요청이 아니라 evidence gate 역할을 했습니다.
- “E2E 재현이 된 것인가?”라는 질문은 분리 PoC를 완성된 체인으로 부르지 않게 했습니다.
- “확정될 때까지 계속하자”는 요청은 실제 Policies component와 Graph 경로를 통과하는 통합 PoC로 이어졌습니다.
- “공격 모델이 현실적인가?”라는 질문은 단일 사용자와 shared deployment의 신뢰 주체를 분리하게 했습니다.
- “가능성이 아니라 증명·재현으로 승격하라”는 요청은 stored-flow와 authorization helper 실험을 만들게 했습니다.
- 공개화 재감사는 당시의 강한
e2e표현을 그대로 보존하지 않고 증거가 실제로 통과한 경계까지만 다시 이름 붙이게 했습니다.
따라서 이 연구의 성장은 severity 문구가 계속 커진 과정이 아닙니다. 핵심 취약점은 더 깊은 실행 계층에서 반복 확인했고, 주변 확대 주장은 증거가 부족한 만큼 다시 낮춘 과정입니다.
최종적으로 확립된 조사 원칙
- 정적 blind spot, validation bypass, 실행 sink와 transport-level exploit은 서로 다른 증거 계층입니다.
- Negative control은 “검증이 없다”와 “검증 범위가 불완전하다”를 구분합니다.
Flow.data에 값이 남는 것과 실제 guarded tool 호출 시 Python이 실행되는 것을 분리합니다.
- 실제 helper 함수 호출은 실제 network·authentication·database E2E를 자동으로 뜻하지 않습니다.
- 설정 의존 amplifier는 기본 공격 모델과 섞지 않습니다.
- 원본 파일명이나 당시 표현보다 재감사한 evidence boundary를 우선합니다.
- 발견 snapshot, 공식 affected 범위, merged fix와 fixed release를 서로 다른 source of truth로 추적합니다.
- 더 강한 주장을 시도했다가 증거가 부족하면 삭제로 숨기지 않고 조건부·미입증·제외로 재분류합니다.
Confirmed core — 핵심 자체 재현과 공식 판정
- flow 생성 권한이 있는 인증 사용자가 자신의 flow에 조작된 동적 guard 코드를 저장할 수 있었다.
- 동적
CodeInput은 주 컴포넌트 코드의 custom-component 해시 게이트에 포함되지 않았다.
allow_custom_components=false에서도 값이Flow.data에 저장되고 실행 가능한 구조로 복원됐다.
- 실제 실행은 저장 즉시가 아니라 Guard 모드의 guarded tool 호출 시 발생했다.
- 코드는 Langflow 백엔드 프로세스의 권한으로 실행됐다.
- v1.10.1의 핵심 수정은 제한 정책이 활성화된 경우 ToolGuard import·load·exec 이전에 실행을 거부한다.
Conditional chains — 조건부 helper-level 확대 경로
Agentic MCP를 통한 cross-tenant field update
IBM CNA는 agentic MCP의
update_flow_component_field가 호출자가 제공한 user_id를 신뢰해 타 사용자의 flow에 값을 주입할 수 있는 확대 경로를 기술한다. 취약 시점 소스에서 이 helper는 flow_id_or_name, component_id, field_name, new_value, user_id를 호출자 입력으로 받았다.이 경로는 confirmed core의 필수 조건이 아니다. MCP helper 접근, 대상 사용자·flow·필드 식별, 피해 flow의 후속 Guard 실행이 추가로 필요하다.
CVE 핵심 fix와 cross-tenant hardening은 별도다. v1.10.1은 ToolGuard RCE sink를 차단했고, caller-supplied
user_id 제거는 후속 1641b28...에서 이뤄졌다. 따라서 v1.10.1이 MCP 사용자 바인딩 자체를 수정했다고 쓰지 않는다.공개 flow 및 인증 완화 설정
CNA는 공개 flow와
AUTO_LOGIN=true, NEW_USER_IS_ACTIVE=true 같은 특정 설정이 결합될 경우 인증 요구가 낮아질 수 있다고 설명한다. 이는 배포별 조건부 체인이며 모든 기본 설치가 무인증 RCE라는 의미가 아니다.구분 | 판정 | 문서 표현 |
인증 사용자 + 자기 flow + 동적 CodeInput | Confirmed core | 기본 취약점으로 기술 |
Agentic MCP caller-supplied user_id | Conditional helper-level chain | 추가 조건과 별도 hardening을 함께 기술 |
공개 flow + AUTO_LOGIN/신규 사용자 설정 | Conditional deployment chain | 특정 구성에서 인증 장벽이 낮아질 수 있다고 제한 |
기본 설정 무인증 RCE | 미입증 | 단정 금지 |
컨테이너 탈출·호스트 root | 미입증 | 프로세스 권한 범위와 분리 |
실제 대규모 악용 | 미확인 | KEV 미등재와 실제 악용 부재를 동일시하지 않음 |
영향
임의 Python 코드는 Langflow 백엔드 프로세스의 권한과 배포 환경의 경계 안에서 실행된다. 가능한 영향은 다음과 같다.
- 백엔드가 읽을 수 있는 데이터·설정·자격증명의 노출
- flow, 데이터베이스 또는 애플리케이션 상태 변조
- 프로세스가 접근 가능한 내부 네트워크로의 추가 접근
- 서비스 중단, 저장형 지속성 및 조건부 타 사용자 flow 오염
IBM CNA는 기밀성·무결성·가용성을 모두
High, Scope를 Changed로 산정했다. 이는 컨테이너 탈출을 뜻하지 않으며 실제 범위는 서비스 계정, 격리, 파일·네트워크·IAM 권한에 제한된다.CISA ADP의 SSVC 값은
Exploitation:none, Automatable:no, Technical Impact:total이다. 2026-08-27 공개된 CISA KEV 카탈로그에도 이 CVE는 없었다. 이는 “확인된 KEV 등재가 없다”는 뜻이지 실제 악용이 절대 없다는 증거는 아니다.수정 분석과 커밋 계보
역할 | 커밋 | 해석 |
참조 스냅샷 | 94981c443d4918517b9e8163d70fc598dc33a32d | Docker 이미지 갱신. CVE fix가 아님 |
최초 PR head | 955c8b1195f9e627231befd87bf2989718b02aad | ToolGuard 실행 정책 게이트의 최초 공개 구현 |
Primary merged fix | 0b14a6b06b8a2802c9a883d6ad98ef3489511862 | 리뷰 후 fail-closed 보강과 테스트를 포함해 #13701로 병합된 핵심 수정 |
Fixed release | a66b75ac2603b26988fb6be95303fdc61f807190 | v1.10.1 태그가 직접 가리키는 릴리스 커밋. 0b14a6b를 포함 |
별도 cross-tenant hardening | 1641b28f33e2c47b9a0c6855922d44d1b8418b9d | Agentic MCP 도구를 인증 사용자 ID에 바인딩한 후속 multi-tenant 보강 |
병합된
0b14a6b...는 _code_execution_allowed()를 추가한다. allow_custom_components=false이면 guard_tools()가 ToolGuard import·load·exec 전에 중단한다. 리뷰 과정에서 settings service가 None인 경우도 fail-closed로 보강했고, 차단 시 runtime을 import하지 않는지와 허용 설정의 정상 경로를 회귀 테스트로 고정했다.IBM 공지는 검증 범위 확장으로 요약하지만, upstream 구현은 동적 필드를 각각 해시화하지 않고 실행 정책 게이트에서 sink 도달을 차단한다.
공식 workaround는 없다. 취약 버전에서
allow_custom_components=false만 재설정하는 것은 해결책이 아니며, 0b14a6b...를 포함한 v1.10.1 이상 지원 릴리스로 업그레이드해야 한다.안전한 공개 증거 세트
공개 증거는 foxirain/CVE-public — CVE-2026-9135에 보존한다.
재현 세트는 공격 페이로드보다 신뢰 경계와 관찰 한계를 고정하는 데 초점을 둔다.
- 발견·검증 snapshot
94981c...의 관련 취약 source 8개
- validation, ToolGuard sink, Policies component, Graph, stored-flow core를 나눈 marker-only PoC 5개
FakeSession과 직접 helper 호출 한계를 명시한 cross-tenant supplemental PoC 2개
- 2026-05-07 historical core 관찰과 2026-08-30 fresh recreation 결과·한계
- CNA record, IBM bulletin metadata, primary merged fix와 별도 user-binding hardening diff
- raw session·private submission draft/report의 위치를 공개하지 않고 크기·SHA-256으로 연결한 provenance
공개 PoC에서 악성 payload의 유일한 의도된 부작용은 임시 marker 생성이다. Harness와 Langflow/ToolGuard 초기화는 격리된 임시 작업 경로에 guard source, result와 config를 만들 수 있다. 실제 타 사용자 식별자, shell, 외부 callback, credential 접근, 데이터 반출은 사용하지 않는다.
검증 항목 | 환경 | 안전한 기대 결과 |
Validation bypass | 94981c..., allow_custom_components=false | trusted main code는 유지하면서 동적 code field가 검증을 통과하고 main-code 변경 대조군은 차단 |
실행 sink와 integration | 동일 snapshot, ToolGuard 0.2.17 | runtime enter 또는 guarded tool 호출 뒤에만 임시 marker 관찰 |
Stored-flow core | in-memory Flow model과 application run core | 동적 field 보존과 run-core 흐름을 확인; 실제 DB transaction·HTTP 요청은 검증 범위 밖 |
Fixed release 분석 | 0b14a6b... diff와 v1.10.1 ancestry | import·load·exec 이전 fail-closed gate와 regression test를 정적 확인; 공개 세트가 fixed binary E2E를 주장하지 않음 |
Conditional helper test | synthetic UUID, FakeSession, 직접 helper 호출 | source/helper ownership 흐름만 기록; 인증 MCP transport·real DB·network E2E는 미검증 |
타임라인
날짜 | 사건 |
2026-05-01 | Langflow v1.9.2 릴리스, 태그 ea3eae8b9e011ff85d7f92f00cf07916dccf755e |
2026-05-06 | CNA 설명에 포함된 94981c... 작성. Docker 관련 커밋이며 보안 수정이 아님 |
2026-05-20 | CVE-2026-9135 예약 |
2026-06-09 | 마지막 공식 영향 릴리스 v1.10.0, 태그 9690e69e86954ba9856c6ef06db7721d5ffa4bab |
2026-06-16 | 최초 PR head 955c8b1... 작성 |
2026-06-19 | 리뷰 보강을 포함한 primary fix 0b14a6b...가 PR #13701로 병합 |
2026-06-23 | 수정 버전 v1.10.1 공개, 태그 a66b75a... |
2026-07-02 | IBM Security Bulletin 최초 공개 |
2026-07-13 | 별도 multi-tenant hardening 1641b28... 병합 |
2026-07-17 | IBM CNA CVE 레코드 공개 및 NVD 접수 |
2026-07-20 | NVD 초기 분석 및 CISA ADP enrichment |
2026-07-23 | NVD 최종 변경 |
2026-08-27 | 검토한 CISA KEV 카탈로그 기준 미등재 |
2026-08-30 | 본 문서 정확성 검토 기준일 |
업스트림 수정과 릴리스가 CVE 공개보다 먼저 이뤄졌다. 따라서 CVE 공개일, 벤더 공지일, fix 작성·병합일, fixed release 공개일을 서로 바꿔 쓰지 않는다.
Attribution 및 provenance
공개 CVE의 CNA는 IBM Corporation이다. IBM Bulletin의 Acknowledgement에는 공개적으로 확인되는 연구자 이름이 없으므로, 현재 공개 1차 자료만으로 특정 개인이나 팀을 “IBM이 인정한 공식 발견자”라고 단정하지 않는다.
Upstream PR 작성자는 수정 provenance이며 발견자 attribution과는 별개다. Whoami/CVE 원본 세션과 공개 포트폴리오도 독립 발견·재현 provenance로 기록하되 벤더 공식 credit과 구분한다.
증거 우선순위는 IBM CNA·Bulletin의 공식 판정, upstream PR·commit·tag의 코드 계보, 원본 조사 세션, 비식별화된 public evidence pack 순이다.
필수 provenance는 원본 세션 ID·시각, 버전·image digest·checkout SHA, 정책·권한, 비식별화 로그, artifact SHA-256·redaction 기록, 벤더 보고·패치·공개 시점과 저장소 commit이다.
권장 attribution 문구는 다음과 같다.
내부 provenance: whoami/CVE 원본 세션에서 독립적으로 발견·재현한 과정과 후속 공개 증거 세트가 보존되어 있다.
공개 attribution: IBM CNA 및 IBM Security Bulletin에는 현재 명시적인 연구자 credit이 없다.