1P by GN⁺ | ★ favorite | 댓글 1개
  • EEF CNA가 공개한 CVE의 35.8%는 제어되지 않은 리소스 소비이며, BEAM 생태계에서는 반복적인 atom 고갈이 큰 비중을 차지함
  • Atom 고갈은 서비스 거부 취약점으로, atom은 가비지 컬렉션되지 않고 전역 테이블에 쌓이며 테이블이 가득 차면 VM이 크래시됨
  • 사용자 입력처럼 가능한 값의 집합이 유한하다고 보장되지 않는 데이터에서 atom을 만들면 DoS 위험이 생기며, URI scheme도 예외가 아님
  • 위험은 binary_to_atom/1, String.to_atom/1 같은 명시적 호출뿐 아니라 JSON 키 atom 디코딩과 문자열 보간 기반 동적 생성에도 존재함
  • 안전한 처리는 런타임 새 atom 생성을 피하고, 알려진 값은 명시적 조회 테이블이나 to_existing_atom 계열로 제한하며 린터로 점검해야 함

Atom 고갈이 만드는 서비스 거부 취약점

  • EEF CNA가 공개한 CVE 중 35.8% 는 제어되지 않은 리소스 소비이며, BEAM 생태계에서는 반복적인 atom 고갈 문제가 큰 비중을 차지함 {p:36}
  • 현재 분포는 EEF CNA의 Common Weaknesses 페이지에서 확인 가능함
  • Atom 고갈은 서비스 거부(DoS) 취약점임
    • Atom은 가비지 컬렉션되지 않음
    • 전역 atom 테이블에 저장됨
    • 테이블이 가득 차면 VM이 크래시됨
  • 유한하지 않은 값, 특히 사용자 입력에서 atom을 만들면 잠재적인 DoS가 됨
  • 위험은 명백한 호출에만 한정되지 않음
    • Erlang의 binary_to_atom/1, list_to_atom/1
    • Elixir의 String.to_atom/1, List.to_atom/1
  • 덜 눈에 띄는 위험 패턴도 존재함
    • Erlang에서 보간을 통한 동적 atom 생성:
      % Erlang: 보간을 통한 동적 atom 생성
      list_to_atom("field_" ++ UserInput)
      
    • Elixir에서 JSON 키를 atom으로 디코딩:
      
      
      
      # Elixir: JSON을 atom 키로 디코딩
      Jason.decode(json, keys: :atoms)
      
    • Elixir에서 보간을 통한 동적 atom 생성:
      
      
      
      # Elixir: 보간을 통한 동적 atom 생성
      :"field_#{user_input}"
      

안전한 처리 방식과 점검 대상

  • Atom 고갈 취약점은 단순한 부주의가 아니라, 입력이 통제되거나 유한하다고 가정한 코드에서 자주 생김
  • URI scheme은 대표적인 예시임
    • 처리할 scheme이 몇 개뿐이라고 느껴질 수 있음
    • 값이 외부 입력에서 오면 가능한 집합이 더 이상 유한하다고 보장되지 않음
  • 입력에서 atom을 만드는 코드는 가능한 값의 집합이 유한하고, 알려져 있으며, 강제되는 경우가 아니면 안전하지 않음
  • 가장 안전한 접근은 런타임에 새 atom을 만들지 않는 것임
  • 허용 값이 알려져 있다면 명시적 조회 테이블을 쓰는 편이 안전함
    % Erlang
    case Scheme of
        <<"http">> -> http;
        <<"https">> -> https;
        _ -> error
    end
    
  • 조회 테이블이 실용적이지 않을 때는 새 atom을 만들지 않고 기존 atom만 사용하는 변형을 써야 함
    • 이 함수들은 새 atom을 생성하지 않고 오류를 발생시킴
    % Erlang
    binary_to_existing_atom(Value)
    list_to_existing_atom(Value)
    
    
    
    
    # Elixir
    String.to_existing_atom(value)
    List.to_existing_atom(value)
    
  • 린터는 취약점으로 이어지기 전 위험 패턴을 잡는 데 도움이 됨
    • Elixir 프로젝트에서는 Credo의 Credo.Check.Warning.UnsafeToAtom 활성화를 고려할 수 있음
    • 이 검사는 String.to_atom/1, List.to_atom/1, Module.concat/1,2, keys: :atoms를 사용하는 Jason.decode/2의 안전하지 않은 호출을 표시함
    • 해당 검사는 기본적으로 비활성화되어 있음
  • Erlang 또는 Elixir 프로젝트 유지보수자는 바이너리, 문자열, JSON 키, URI 구성요소, 헤더, 설정 값에서 atom을 생성하는 코드를 검색해야 함
  • 이 취약점 범주는 CVE가 되기 전에 고치기 쉬운 유형 중 하나임
  • 더 자세한 지침은 EEF Security Working Group의 atom 고갈 방지 가이드에 정리되어 있음

댓글과 토론

Lobste.rs 의견들
  • Ruby에서 Symbol가비지 컬렉션 대상이 되기 전 상황과 비슷하게 들림

  • 제목이 이해가 안 감. 이건 확실히 footgun처럼 보임

    • 제목의 요지는 atom 고갈을 “그냥 footgun”이라고 부르면 문제의 심각성이 과소평가된다는 뜻 같음
    • 기억이 맞다면, Erlang을 매일 쓰진 않지만 atom은 가비지 컬렉션되지 않음
      “Ruby에도 Erlang atom 같은 symbol이 있지 않나?”라고 생각하면 맞지만, Ruby는 symbol을 가비지 컬렉션함
      게다가 기본적으로 Erlang atom이 저장되는 조회 테이블은 최대 1,048,576개만 허용함
      폼 같은 사용자 입력으로 atom을 동적으로 생성하면 매우 위험하고, 소프트웨어가 서비스 거부 공격에 노출됨
    • “그냥” 단순한 footgun보다 더 큰 문제라는 뜻으로 이해했음
      다만 내 경험상 “footgun” 자체가 꽤 넓은 표현이라, 어느 쪽이든 제목 문구는 어색함
    • 맞음, 그것도 엄청나게 큰 footgun처럼 보임
  • 뭔가 핵심적인 부분의 설계나 구현이 나쁜 것처럼 들려서 놀라움. 인터넷에서 계속 칭찬받던 언어라 더 의외임

    • BEAM atom은 본질적으로 인터닝된 문자열이고, 전역 바이트↔정수 테이블을 가짐
      그 테이블에 참조 카운트를 추가하면 비용이 크고, 수십 년간 존재해 온 코드의 확장 특성이 바뀜
      atom 최대 개수는 기본값이 100만 개이며, VM 시작 시점에 정해짐
      함정이긴 하지만 피하기 어렵지는 않음. 오래전부터 권장사항은 “사용자 입력으로 atom을 만들지 말라”였음
      예를 들어 JSON을 파싱한다면 보통 키를 atom으로 변환하지 않거나, 이미 존재하는 atom일 때만 변환함. 이렇게 하면 atom 키로 패턴 매칭할 수 있고, 코드 로딩으로 해당 atom들이 이미 만들어져 있으며, 포괄 절은 atom 대신 문자열을 받을 수 있음
    • Erlang은 특수한 경우에 전문 프로그래머들이 쓰던 니치 언어였다는 점을 감안해야 함
      Elixir는 훨씬 더 대중적이라, Erlang 개발자는 이를 알고 있을 가능성이 크지만 Elixir 개발자는 모를 가능성이 있음
  • 개인적으로는 atom을 그런 식으로 쓰는 것 자체가 이상하게 느껴짐. Erlang의 atom을 대략 C의 enum 타입과 비슷하게 이해하고 있기 때문임
    특정 방식으로 단어를 입력하면 내부적으로 enum이 되는 편의 기능이라고 봄
    글에서는 사용자 입력을 언급하지만, 애초에 사용자 입력으로 새 enum 타입을 만들고 싶을 만한 사용 사례가 왜 있는지 모르겠음. 쓰임새가 극히 좁아 보임
    옆 댓글들은 파싱을 말하지만, 이상적으로는 미리 알려진 자료 구조를 파싱하는 것 아닌가? 뭔가 놓치고 있는 느낌임

    • 질문과는 조금 빗나가지만, K에서는 symbol이 전역으로 인터닝되고, Erlang처럼 K 프로세스에서 symbol 테이블을 고갈시켜 죽일 수 있음
      그 언어에서 symbol을 단순히 빠른 동등성 비교 이상의 별도 타입으로 두는 장점은 최소 두 가지임. symbol은 atom, 즉 문자들의 리스트형 시퀀스가 아니라 원자적 단위라 여러 연산자가 다르게 취급하고, symbol은 벡터화되어 단일 타입 리스트에 조밀하게 저장될 수 있음
      K와 Q에서는 데이터베이스 테이블의 열을 벡터화된 타입으로 표현하는 것이 매우 바람직함. 지역성이 좋고, 메모리를 더 효율적으로 쓰며, 여러 연산자에 빠른 경로가 많기 때문임. 하지만 symbol 테이블 제약 때문에, 카디널리티가 높은 열에 symbol을 쓸 때는 조심해야 함
      알려진 스키마의 JSON을 파싱한다면 symbol은 딕셔너리 키로 훌륭하고, k2/k3에서는 사실상 필수임. 하지만 정체를 모르는 JSON이라면 사용자 입력에서 오면 안 됨
      일부 K 방언에서는 symbol 길이를 짧게 제한해서 64비트 값으로 포장하고 옮길 수 있게 함. 일반성을 포기하는 대신 symbol 테이블 자체가 필요 없어짐
  • “통제된 입력”과 “통제되지 않은 입력”의 구분은 보안에서 null 여부 같은 것처럼 느껴짐
    webpack-plugin-less-css가 신뢰할 수 없는 CSS 파일을 받으면 서비스 거부가 난다는 식의 항목을 보면 CVE 피로감이 꽤 큼
    그래도 여기서 더 나은 경계 표시가 있으면 좋겠음. 예를 들어 문자열 연결을 거치면 어떤 안전 속성이 보존되는지 같은 조합 규칙도 잘 다룰 수 있으면 좋음
    그리고 HTTP POST에서 받은 것들을 잔뜩 SafeString 처리했다면, 그건 어느 정도 본인 책임