1 คะแนน โดย GN⁺ 4 시간 전 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • EEF CNA ที่เปิดเผย CVE ออกมา 35.8% เป็นปัญหาการใช้ทรัพยากรโดยไม่มีการควบคุม และในระบบนิเวศ BEAM ปัญหา atom หมดซ้ำๆ คิดเป็นสัดส่วนใหญ่
  • Atom หมด เป็นช่องโหว่แบบปฏิเสธการให้บริการ โดย atom จะไม่ถูก garbage collect และจะสะสมอยู่ในตาราง global เมื่อเต็มแล้ว VM จะ crash
  • หากสร้าง atom จากข้อมูลอย่าง อินพุตผู้ใช้ ที่ไม่อาจรับประกันได้ว่าชุดค่าที่เป็นไปได้นั้นมีขอบเขตจำกัด ก็จะเกิดความเสี่ยง DoS และ URI scheme ก็ไม่ใช่ข้อยกเว้น
  • ความเสี่ยงไม่ได้มีแค่การเรียกแบบชัดเจนอย่าง binary_to_atom/1, String.to_atom/1 เท่านั้น แต่ยังมี การถอดรหัสคีย์ JSON เป็น atom และการสร้างแบบไดนามิกจาก string interpolation ด้วย
  • วิธีที่ปลอดภัยคือหลีกเลี่ยงการสร้าง atom ใหม่ระหว่างรันไทม์ และสำหรับค่าที่ทราบอยู่แล้วให้จำกัดไว้ด้วย ตาราง lookup แบบระบุชัดเจน หรือฟังก์ชันตระกูล to_existing_atom พร้อมตรวจสอบด้วย linter

ช่องโหว่ DoS ที่เกิดจาก atom หมด

  • ในบรรดา CVE ที่ EEF CNA เปิดเผย 35.8% เป็นปัญหาการใช้ทรัพยากรโดยไม่มีการควบคุม และในระบบนิเวศ BEAM ปัญหา atom หมดที่เกิดซ้ำมีสัดส่วนสูง {p:36}
  • ดูการกระจายปัจจุบันได้ที่หน้า Common Weaknesses ของ EEF CNA
  • Atom หมด เป็นช่องโหว่แบบปฏิเสธการให้บริการ (DoS)
    • Atom จะไม่ถูก garbage collect
    • ถูกเก็บไว้ในตาราง atom แบบ global
    • เมื่อ table เต็ม VM จะ crash
  • การสร้าง atom จากค่าที่ไม่มีขอบเขตจำกัด โดยเฉพาะ อินพุตผู้ใช้ อาจกลายเป็น DoS ได้
  • ความเสี่ยงไม่ได้จำกัดอยู่แค่การเรียกที่เห็นได้ชัด
    • binary_to_atom/1, list_to_atom/1 ของ Erlang
    • String.to_atom/1, List.to_atom/1 ของ Elixir
  • ยังมีรูปแบบความเสี่ยงที่สังเกตได้ยากกว่า
    • การสร้าง atom แบบไดนามิกผ่าน interpolation ใน Erlang:
      % Erlang: 보간을 통한 동적 atom 생성
      list_to_atom("field_" ++ UserInput)
      
    • การถอดรหัสคีย์ JSON เป็น atom ใน Elixir:
      
      
      
      # Elixir: JSON을 atom 키로 디코딩
      Jason.decode(json, keys: :atoms)
      
    • การสร้าง atom แบบไดนามิกผ่าน interpolation ใน Elixir:
      
      
      
      # Elixir: 보간을 통한 동적 atom 생성
      :"field_#{user_input}"
      

วิธีจัดการอย่างปลอดภัยและจุดที่ควรตรวจสอบ

  • ช่องโหว่ atom หมดไม่ใช่แค่ความประมาทธรรมดา แต่เกิดขึ้นบ่อยในโค้ดที่สมมติว่าอินพุตถูกควบคุมหรือมีขอบเขตจำกัด
  • URI scheme เป็นตัวอย่างที่ชัดเจน
    • อาจรู้สึกว่ามี scheme ที่ต้องรองรับอยู่เพียงไม่กี่แบบ
    • แต่ถ้าค่านั้นมาจากอินพุตภายนอก ก็ไม่อาจรับประกันได้อีกต่อไปว่าชุดค่าที่เป็นไปได้มีขอบเขตจำกัด
  • โค้ดที่สร้าง atom จากอินพุตจะไม่ปลอดภัย เว้นแต่ชุดค่าที่เป็นไปได้จะ มีขอบเขตจำกัด, เป็นที่รู้จัก, และมีการบังคับใช้อย่างชัดเจน
  • แนวทางที่ปลอดภัยที่สุดคือไม่สร้าง atom ใหม่ระหว่างรันไทม์
  • หากทราบค่าที่อนุญาตอยู่แล้ว การใช้ ตาราง lookup แบบระบุชัดเจน จะปลอดภัยกว่า
    % Erlang
    case Scheme of
        <<"http">> -> http;
        <<"https">> -> https;
        _ -> error
    end
    
  • หากตาราง lookup ใช้งานจริงได้ไม่สะดวก ควรใช้รูปแบบที่อ้างอิงเฉพาะ 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)
    
  • linter ช่วยจับรูปแบบเสี่ยงก่อนจะกลายเป็นช่องโหว่ได้
    • ในโปรเจ็กต์ Elixir อาจพิจารณาเปิดใช้ Credo.Check.Warning.UnsafeToAtom ของ Credo
    • กฎนี้จะทำเครื่องหมายการเรียกที่ไม่ปลอดภัยของ String.to_atom/1, List.to_atom/1, Module.concat/1,2 และ Jason.decode/2 ที่ใช้ keys: :atoms
    • การตรวจนี้ถูกปิดไว้โดยค่าเริ่มต้น
  • ผู้ดูแลโปรเจ็กต์ Erlang หรือ Elixir ควรค้นหาโค้ดที่สร้าง atom จาก binary, string, คีย์ JSON, องค์ประกอบของ URI, header และค่าคอนฟิก
  • ช่องโหว่กลุ่มนี้เป็นหนึ่งในประเภทที่แก้ได้ง่ายก่อนจะกลายเป็น CVE
  • ดูคำแนะนำเพิ่มเติมได้ใน คู่มือป้องกัน atom หมด ของ EEF Security Working Group

1 ความคิดเห็น

 
GN⁺ 4 시간 전
ความคิดเห็นจาก Lobste.rs
  • ฟังดูคล้ายกับสถานการณ์ก่อนที่ Symbol ใน Ruby จะถูก garbage collection

  • อ่านชื่อเรื่องแล้วไม่เข้าใจ อันนี้ดูเป็น footgun ชัด ๆ

    • ใจความของชื่อเรื่องน่าจะเป็นว่า ถ้าเรียก atom exhaustion ว่าเป็น “แค่ footgun” จะเป็นการประเมินความร้ายแรงของปัญหาต่ำไป
    • ถ้าจำไม่ผิด ถึงจะไม่ได้ใช้ Erlang ทุกวัน แต่ atom จะไม่ถูก garbage collection
      ถ้าคิดว่า “Ruby ก็มี symbol คล้าย Erlang atom ไม่ใช่เหรอ?” ก็ใช่ แต่ Ruby จะทำ garbage collection ให้ symbol
      แถมโดยปกติ ตาราง lookup ที่เก็บ Erlang atom ก็อนุญาตได้สูงสุดแค่ 1,048,576 รายการ
      ถ้าสร้าง atom แบบไดนามิกจากอินพุตผู้ใช้ เช่น ฟอร์ม จะอันตรายมาก และทำให้ซอฟต์แวร์เปิดช่องให้ถูกโจมตีแบบปฏิเสธการให้บริการได้
    • ผมเข้าใจว่าเขาหมายถึงมันเป็นปัญหาที่ใหญ่กว่า footgun แบบ “ธรรมดา”
      แต่จากประสบการณ์ของผม คำว่า “footgun” เองก็เป็นคำที่กว้างพอสมควร ดังนั้นไม่ว่าจะมองแบบไหน ถ้อยคำในชื่อเรื่องก็ยังฟังแปลก ๆ
    • ใช่ มันดูเป็น footgun ที่ใหญ่มากด้วย
  • ค่อนข้างแปลกใจ เพราะมันฟังดูเหมือนมีบางส่วนระดับแกนกลางที่ ออกแบบหรือ implement ได้ไม่ดี ทั้งที่เป็นภาษาที่โดนชมในอินเทอร์เน็ตอยู่ตลอด

    • atom ของ BEAM โดยพื้นฐานแล้วคือ สตริงที่ถูก interned และมีตาราง byte↔integer แบบ global
      ถ้าจะเพิ่ม reference count ให้ตารางนั้น ต้นทุนจะสูงมาก และจะเปลี่ยนคุณสมบัติด้านการสเกลของโค้ดที่มีมานานหลายสิบปี
      จำนวน atom สูงสุดโดยค่าเริ่มต้นคือ 1 ล้าน และกำหนดตั้งแต่ตอนเริ่ม VM
      มันเป็นกับดักก็จริง แต่ไม่ได้หลีกเลี่ยงยากมานานแล้ว แนวปฏิบัติที่แนะนำคือ “อย่าสร้าง atom จากอินพุตผู้ใช้”
      ตัวอย่างเช่น ถ้าพาร์ส JSON ปกติก็จะไม่แปลง key เป็น atom หรือไม่ก็แปลงเฉพาะเมื่อเป็น atom ที่มีอยู่แล้ว แบบนี้คุณยัง pattern match ด้วย key แบบ atom ได้ และ atom เหล่านั้นก็ถูกสร้างไว้แล้วจากการโหลดโค้ด ขณะที่ clause แบบครอบคลุมทั้งหมดสามารถรับสตริงแทน atom ได้
    • ต้องคำนึงด้วยว่า Erlang เป็น ภาษาเฉพาะทาง ที่ในบางกรณีใช้โดยโปรแกรมเมอร์มืออาชีพ
      Elixir เป็นที่นิยมในวงกว้างกว่ามาก ดังนั้นนักพัฒนา Erlang มักจะรู้เรื่องนี้ แต่นักพัฒนา Elixir อาจไม่รู้
  • ส่วนตัวรู้สึกว่า การใช้ atom แบบนั้นมันแปลกอยู่แล้ว เพราะผมเข้าใจ atom ของ Erlang ว่าคล้าย ชนิด enum ในภาษา C โดยประมาณ
    ผมมองว่ามันเป็นฟีเจอร์อำนวยความสะดวกที่ทำให้เมื่อพิมพ์คำในรูปแบบหนึ่งแล้ว ภายในจะกลายเป็น enum
    ในบทความพูดถึงอินพุตผู้ใช้ แต่ตั้งแต่แรกผมก็ไม่เข้าใจว่าทำไมถึงจะมีกรณีใช้งานที่อยากสร้างชนิด enum ใหม่จากอินพุตผู้ใช้อยู่แล้ว มันดูเป็นการใช้งานที่เฉพาะมาก
    คอมเมนต์ข้าง ๆ พูดถึงการพาร์ส แต่ในอุดมคติแล้วก็น่าจะพาร์สโครงสร้างข้อมูลที่รู้ล่วงหน้าไม่ใช่หรือ? รู้สึกเหมือนผมกำลังพลาดอะไรบางอย่าง

    • อาจจะไม่ตรงกับคำถามนัก แต่ใน K symbol ก็ถูก intern แบบ global เช่นกัน และเหมือน Erlang ตรงที่สามารถทำให้ ตาราง symbol หมดจนโปรเซส K ตายได้
      ในภาษานั้น การมี symbol เป็นชนิดแยกต่างหาก ไม่ใช่แค่เพื่อการเปรียบเทียบความเท่ากันที่เร็วขึ้น มีข้อดีอย่างน้อยสองอย่าง symbol เป็น atom กล่าวคือเป็นหน่วยอะตอม ไม่ใช่ลำดับแบบลิสต์ของตัวอักษร จึงมี operator หลายตัวที่ปฏิบัติต่อมันต่างออกไป และ symbol ยัง vectorized ได้ ทำให้เก็บแบบหนาแน่นในลิสต์ชนิดเดียวได้
      ใน K และ Q การแทนคอลัมน์ของตารางฐานข้อมูลเป็นชนิดที่ถูก vectorize เป็นสิ่งที่พึงประสงค์มาก เพราะ locality ดีกว่า ใช้หน่วยความจำมีประสิทธิภาพกว่า และมี fast path สำหรับ operator หลายตัว แต่เพราะข้อจำกัดของตาราง symbol จึงต้องระวังเมื่อนำ symbol ไปใช้กับคอลัมน์ที่มี cardinality สูง
      ถ้ากำลังพาร์ส JSON ที่มี schema ชัดเจน symbol เหมาะมากสำหรับใช้เป็น key ของ dictionary และใน k2/k3 แทบจะจำเป็นเลย แต่ถ้าเป็น JSON ที่ไม่รู้ว่าคืออะไร ก็ไม่ควรมาจากอินพุตผู้ใช้
      ในบาง dialect ของ K จะจำกัดความยาวของ symbol ให้สั้น เพื่อให้แพ็กและย้ายเป็นค่า 64 บิตได้ ทำให้ไม่ต้องมีตาราง symbol ไปเลย โดยแลกกับการลดความเป็นทั่วไปลง
  • ความต่างระหว่าง “อินพุตที่ควบคุมได้” กับ “อินพุตที่ควบคุมไม่ได้” ในเรื่องความปลอดภัยนั้น ให้ความรู้สึกเหมือนเรื่องการเป็น null หรือไม่เป็น null
    เวลาเห็นรายการแนว ๆ ว่า webpack-plugin-less-css ถ้าได้รับไฟล์ CSS ที่ไม่น่าเชื่อถือแล้วจะเกิดการปฏิเสธการให้บริการ ก็รู้สึก ล้ากับ CVE มากพอสมควร
    ถึงอย่างนั้นก็ยังอยากให้มีการระบุเส้นแบ่งที่ดีกว่านี้ เช่น ถ้าผ่านการต่อสตริงแล้ว คุณสมบัติด้านความปลอดภัยอะไรบ้างที่ยังคงอยู่ ก็ควรมีการอธิบายกฎการประกอบแบบนี้ให้ดี
    และถ้าคุณเอาสิ่งที่ได้จาก HTTP POST มากองรวมกันแล้วทำเป็น SafeString หมด แบบนั้นก็ต้องโทษตัวเองในระดับหนึ่ง