:erlang.binary_to_term을 안전하게 쓰려면?

외부 입력엔 JSON을 쓰고, 불가피하면 Plug.Crypto.non_executable_binary_to_term(bin, [:safe])를 쓰세요.

Baryon Labs · 2026-10-23 작성·검증 · 근거: elixir-security-audit 0.1.0 패턴 카탈로그와 테스트 앱

왜 위험한가요?

  • 디코딩 과정에서 atom이 생성됩니다 → atom 고갈 DoS.
  • fun(익명 함수)을 복원할 수 있습니다. Enumerable은 함수에도 구현되어 있어서, 리스트를 기대한 자리에 fun이 들어오면 Enum.map/2 같은 호출만으로 공격자가 고른 함수가 실행됩니다(RCE).
  • [:safe] 옵션은 새 atom·새 외부 함수 참조 생성을 막지만, 이미 로드된 모듈의 fun은 막지 못합니다. 그래서 Plug는 Plug.Crypto.non_executable_binary_to_term/2를 제공합니다(fun이 있으면 ArgumentError).
방법새 atom 생성fun 실행 위험권장
:erlang.binary_to_term(bin)막지 못함막지 못함외부 입력에 사용 금지
:erlang.binary_to_term(bin, [:safe])막음이미 로드된 모듈의 fun은 막지 못함단독으로는 부족
Plug.Crypto.non_executable_binary_to_term(bin, [:safe])막음fun이 있으면 ArgumentErrorETF가 불가피할 때
JSON + 검증 / Phoenix.Token해당 없음해당 없음우선 선택

패턴 ID는 EXS-DESER-001, 기본 심각도는 Critical, Sobelow 검사는 Misc.BinToTerm입니다.

취약한 코드와 안전한 코드

취약: 클라이언트가 보낸 상태를 그대로 역직렬화

def restore(conn, %{"state" => state}) do
  data = state |> Base.url_decode64!() |> :erlang.binary_to_term()
  render(conn, :show, data: data)
end

수정 (우선순위 순)

# 1) 신뢰 불가 입력에는 ETF 를 쓰지 않는다 — JSON + 검증
{:ok, data} = Jason.decode(state)

# 2) 서버가 만든 값을 클라이언트에 맡겼다 돌려받는 경우 — 서명 토큰
token = Phoenix.Token.sign(MyAppWeb.Endpoint, "state", data)
{:ok, data} = Phoenix.Token.verify(MyAppWeb.Endpoint, "state", token, max_age: 86_400)

# 3) ETF 가 불가피할 때
data = Plug.Crypto.non_executable_binary_to_term(bin, [:safe])

non_executable_binary_to_term/2의 두 번째 인자는 그대로 :erlang.binary_to_term/2에 전달됩니다. [:safe]를 빠뜨리면 atom 생성은 막지 못합니다. 실패하면 ArgumentError가 나므로 rescue해서 4xx로 처리합니다.

테스트 앱에서의 탐지 결과

저장소의 일부러 취약한 Phoenix 앱에서 lib/vuln_app/preferences.ex:10의 :erlang.binary_to_term(raw)는 GET /preferences의 prefs 쿠키에서 도달 가능합니다. Sobelow는 이를 Misc.BinToTerm(high confidence)으로 보고했고, 스캔 요약에서는 grep+sobelow 두 출처가 합쳐진 Critical 항목으로 나타났습니다. 표준 수정안은 Plug.Crypto.non_executable_binary_to_term(bin, [:safe])입니다.

오탐 신호

  • 같은 애플리케이션만 쓰는 저장소(ETS, 노드 로컬 파일)에서 읽고 외부 쓰기 경로가 없음.
  • Phoenix.Token/Plug.Crypto.MessageVerifier 검증을 통과한 뒤의 값(서명 키가 안전하다는 전제).
  • "우리 DB에서 읽는다"는 그 컬럼에 사용자가 쓸 수 있으면 오탐이 아닙니다.

테스트 아이디어

:erlang.term_to_binary(fn -> :pwned end)를 입력으로 보내 4xx 응답(또는 {:error, _})을 확인합니다. 새 atom을 담은 바이너리를 [:safe] 경로에 보내 거부되는지도 확인합니다(인코딩하는 쪽에서 atom이 이미 생기므로 atom 생성 여부가 아니라 거부 여부를 검사).

자주 묻는 질문

[:safe] 옵션만 붙이면 충분한가요?

아니요. [:safe]는 새 atom과 새 외부 함수 참조 생성을 막지만 이미 로드된 모듈의 fun은 막지 못합니다. ETF가 불가피하면 Plug.Crypto.non_executable_binary_to_term(bin, [:safe])를 쓰세요.

서버가 만든 값을 쿠키로 돌려받는 경우는요?

Phoenix.Token.sign/3으로 서명하고 Phoenix.Token.verify/4로 검증해 돌려받습니다. 서명 키가 안전하다는 전제에서 검증을 통과한 값은 오탐으로 판정할 수 있습니다.

더 보기

elixir-security-audit 설치하기 · EXS 패턴 15종 전체 · GitHub 저장소