CI/CD 용어 사전
GitHub Actionspull_request

pull_request_target

시크릿과 쓰기 권한을 갖고 도는 이벤트. 여기서 PR 코드를 실행하면 공급망 취약점이 된다.

PR 에 반응하는 트리거가 둘인데, 권한이 정반대다.

pull_requestpull_request_target
어느 코드의 워크플로가 도나PR 브랜치의 워크플로베이스 브랜치의 워크플로
토큰 권한읽기로 제한쓰기 가능
포크 PR 에서 시크릿접근 불가접근 가능

왜 이런 것이 있나

pull_request 만 있으면 외부 기여자의 PR 에서 아무것도 못 한다 — 라벨을 붙이거나, 커버리지를 코멘트로 남기거나, 검토자를 지정하는 일까지 막힌다. 그런 작업을 하라고 만든 것이 pull_request_target 이다.

핵심은 베이스 브랜치의 워크플로가 돈다는 점이다. 공격자가 PR 로 워크플로 파일을 고쳐도 그것은 실행되지 않는다. 그래서 시크릿을 줘도 안전하다 — PR 코드를 실행하지 않는 한.

여기가 무너지는 지점이다

on: pull_request_target
jobs:
  build:
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}   # ✗ PR 코드를 가져온다
      - run: npm ci && npm test                            # ✗ 그 코드를 실행한다

npm ci 하나로 끝난다. package.json 의 설치 훅에 공격자가 원하는 명령을 넣어 두면, 시크릿과 쓰기 토큰을 가진 채로 그것이 실행된다. 테스트 코드, 빌드 스크립트, 설정 파일 — PR 이 고칠 수 있는 모든 것이 실행 경로다.

이 조합은 워낙 흔해서 별도의 이름이 붙어 있을 정도이고, 실제 유출 사고가 반복해서 나왔다.

안전하게 쓰는 법

  • PR 코드를 체크아웃하지 않는다 — 메타데이터(제목·라벨·작성자)만 다룬다
  • 코드를 봐야 한다면 권한 없는 잡에서 검사하고, 그 결과만 별도 워크플로가 받아 처리한다
  • 시크릿이 필요 없는 일이면 그냥 pull_request 를 쓴다

판단 기준은 하나다 — PR 이 통제하는 것이 실행되는가. 실행된다면 시크릿이 있어서는 안 된다.

검사와 권한을 분리하는 패턴

PR 코드를 정말 실행해야 하는데 결과를 코멘트로 남겨야 한다면, 두 워크플로로 쪼갠다.

① pull_request 로 실행 — 시크릿 없음, 읽기 권한
   PR 코드를 빌드·테스트하고 결과를 아티팩트로 저장

② workflow_run 으로 실행 — 베이스 브랜치 워크플로, 권한 있음
   ①의 아티팩트만 받아 코멘트를 남긴다

신뢰할 수 없는 코드를 실행하는 곳에는 권한이 없고, 권한이 있는 곳에서는 코드를 실행하지 않는다. 두 성질을 한 잡에 섞지 않는 것이 원칙이고, 아티팩트가 그 사이를 잇는 통로다.

②에서도 방심하면 안 된다. ①이 만든 아티팩트 내용은 여전히 PR 이 정한 것이므로, 그 값을 셸에 그대로 넣으면 인젝션이 된다. 경계를 넘어온 값은 계속 데이터로 다룬다.

이미 쓰고 있다면 무엇을 보나

grep -rn "pull_request_target" .github/workflows/

찾았다면 그 워크플로에서 딱 두 가지를 확인한다 — checkout 이 PR 쪽을 가리키는가, 그리고 PR 이 고칠 수 있는 파일이 실행되는가(설치 훅·빌드 스크립트·테스트). 둘 다 아니라면 안전하다.

면접 함정

  • "pull_request_target 은 그냥 더 강력한 버전" → 신뢰 모델이 다르다. 코드 실행과 함께 쓰면 안 된다.
  • "우리 저장소는 비공개라 괜찮다" → 포크가 없더라도 권한 경계가 흐려진다.
  • "테스트만 돌리는 건 안전" → 테스트 코드도 PR 이 고칠 수 있다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — GitHub Actions — 문법과 실행 모델