VYPR
High severityNVD Advisory· Published Sep 21, 2022· Updated Apr 22, 2025

Arbitrary shell execution when extracting or listing files contained in a malicious rpm.

CVE-2022-39224

Description

Arr-pm Ruby library prior to 0.0.12 is vulnerable to OS command injection via a malicious RPM payload compressor field, leading to arbitrary shell execution.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Arr-pm Ruby library prior to 0.0.12 is vulnerable to OS command injection via a malicious RPM payload compressor field, leading to arbitrary shell execution.

Vulnerability

Details CVE-2022-39224 is an OS command injection vulnerability in the Ruby arr-pm library, affecting versions before 0.0.12. The flaw exists in the extract and files methods of the RPM::File class, which unsafely handle the 'payload compressor' field from an RPM file. [1] Instead of validating the field against a whitelist of allowed compressors (gzip, bzip2, xz, zstd, lzma), the library passes it directly to a shell command, enabling injection of arbitrary commands. [1][2]

Exploitation

An attacker can exploit this by crafting a malicious RPM file with a specially crafted 'payload compressor' value containing shell metacharacters. If a system using arr-pm processes such an RPM (e.g., during extraction or file listing), the injected commands execute with the privileges of the process. No authentication is required if the application accepts untrusted RPM files. [1][2]

Impact

Successful exploitation grants an attacker arbitrary shell command execution on the target system, potentially leading to full compromise of the application or server. The vulnerability is particularly dangerous in environments that handle user-uploaded RPM packages. [1]

Mitigation

The issue is fixed in version 0.0.12, which refactors the file listing to avoid shell invocation and instead uses RPM tag data directly. [4] Users should update immediately. As a workaround, ensure that any processed RPMs contain only known payload compressor values (gzip, bzip2, xz, zstd, lzma) by pre-validating with the rpm command-line tool. [1]

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
arr-pmRubyGems
< 0.0.120.0.12

Affected products

2

Patches

3
441764ab706c

Version bump

https://github.com/jordansissel/ruby-arr-pmJordan SisselSep 19, 2022via osv
2 files changed · +2 2
  • arr-pm.gemspec+1 1 modified
    @@ -2,7 +2,7 @@ Gem::Specification.new do |spec|
       files = %x{git ls-files}.split("\n")
     
       spec.name = "arr-pm"
    -  spec.version = "0.0.11"
    +  spec.version = "0.0.12"
       spec.summary = "RPM reader and writer library"
       spec.description = "This library allows to you to read and write rpm " \
         "packages. Written in pure ruby because librpm is not available " \
    
  • arr-pm.gemspec+1 1 modified
    @@ -2,7 +2,7 @@ Gem::Specification.new do |spec|
       files = %x{git ls-files}.split("\n")
     
       spec.name = "arr-pm"
    -  spec.version = "0.0.11"
    +  spec.version = "0.0.12"
       spec.summary = "RPM reader and writer library"
       spec.description = "This library allows to you to read and write rpm " \
         "packages. Written in pure ruby because librpm is not available " \
    
efddd459916e

Merge pull request #15 from jordansissel/validate-payload-compressor

https://github.com/jordansissel/ruby-arr-pmJordan SisselSep 17, 2022via ghsa-ref
3 files changed · +52 2
  • arr-pm.gemspec+2 0 modified
    @@ -19,6 +19,8 @@ Gem::Specification.new do |spec|
     
       spec.add_development_dependency "flores", ">0"
       spec.add_development_dependency "rspec", ">3.0.0"
    +  spec.add_development_dependency "stud", ">=0.0.23"
    +  spec.add_development_dependency "insist", ">=1.0.0"
       #spec.homepage = "..."
     end
     
    
  • lib/arr-pm/file.rb+14 1 modified
    @@ -88,6 +88,14 @@ def payload
         return @payload
       end # def payload
     
    +  def valid_compressor?(name)
    +    # I scanned rpm's rpmio.c for payload implementation names and found the following.
    +    #    sed -rne '/struct FDIO_s \w+ *= *\{/{ n; s/^.*"(\w+)",$/\1/p }' rpmio/rpmio.c
    +    # It's possible this misses some supported rpm payload compressors.
    +
    +    [ "gzip", "bzip2", "xz", "lzma", "zstd" ].include?(name)
    +  end
    +
       # Extract this RPM to a target directory.
       #
       # This should have roughly the same effect as:
    @@ -97,8 +105,13 @@ def extract(target)
         if !File.directory?(target)
           raise Errno::ENOENT.new(target)
         end
    +
    +    compressor = tags[:payloadcompressor]
    +    if !valid_compressor?(compressor)
    +      raise "Cannot decompress. This RPM uses an invalid compressor '#{compressor}'"
    +    end
         
    -    extractor = IO.popen("#{tags[:payloadcompressor]} -d | (cd #{target}; cpio -i --quiet --make-directories)", "w")
    +    extractor = IO.popen("#{compressor} -d | (cd #{target}; cpio -i --quiet --make-directories)", "w")
         buffer = ""
         begin
             buffer.force_encoding("BINARY")
    
  • spec/rpm/file_spec.rb+36 1 modified
    @@ -1,11 +1,14 @@
     # encoding: utf-8
     
     require "arr-pm/file"
    +require "stud/temporary"
    +require "insist"
     
     describe ::RPM::File do
    +  subject { described_class.new(path) }
    +
       context "with a known good rpm" do
         let(:path) { File.join(File.dirname(__FILE__), "../fixtures/pagure-mirror-5.13.2-5.fc35.noarch.rpm") }
    -    subject { described_class.new(path) }
     
         context "#files" do
           let(:files) { [
    @@ -19,4 +22,36 @@
           end
         end
       end
    +
    +  context "#extract" do
    +    # This RPM should be correctly built, but we will modify the tags at runtime to force an error.
    +    let(:path) { File.join(File.dirname(__FILE__), "../fixtures/example-1.0-1.x86_64.rpm") }
    +    let(:dir) { dir = Stud::Temporary.directory }
    +
    +    after do
    +      FileUtils.rm_rf(dir)
    +    end
    +
    +    context "with an invalid payloadcompressor" do
    +      before do
    +        subject.tags[:payloadcompressor] = "some invalid | string"
    +      end
    +
    +      it "should raise an error" do
    +        insist { subject.extract(dir) }.raises(RuntimeError)
    +      end
    +    end
    +
    +    [ "gzip", "bzip2", "xz", "lzma", "zstd" ].each do |name|
    +      context "with a '#{name}' payloadcompressor" do
    +        before do
    +          subject.tags[:payloadcompressor] = name
    +        end
    +
    +        it "should succeed" do
    +          reject { subject.extract(dir) }.raises(RuntimeError)
    +        end
    +      end
    +    end
    +  end
     end
    
c608f8603ebf

Merge pull request #14 from jordansissel/file-list-without-cpio

https://github.com/jordansissel/ruby-arr-pmJordan SisselSep 17, 2022via ghsa-ref
8 files changed · +58 86
  • arr-pm.gemspec+1 0 modified
    @@ -18,6 +18,7 @@ Gem::Specification.new do |spec|
       spec.email = ["jls@semicomplete.com"]
     
       spec.add_development_dependency "flores", ">0"
    +  spec.add_development_dependency "rspec", ">3.0.0"
       #spec.homepage = "..."
     end
     
    
  • arr-pm.gemspec+1 0 modified
    @@ -18,6 +18,7 @@ Gem::Specification.new do |spec|
       spec.email = ["jls@semicomplete.com"]
     
       spec.add_development_dependency "flores", ">0"
    +  spec.add_development_dependency "rspec", ">3.0.0"
       #spec.homepage = "..."
     end
     
    
  • lib/arr-pm/file.rb+6 43 modified
    @@ -195,49 +195,12 @@ def config_files
       # 
       #   % rpm2cpio blah.rpm | cpio -it
       def files
    -    return @files unless @files.nil?
    -
    -    lister = IO.popen("#{tags[:payloadcompressor]} -d | cpio -it --quiet", "r+")
    -    buffer = ""
    -    begin
    -        buffer.force_encoding("BINARY")
    -    rescue NoMethodError
    -        # Do Nothing
    -    end
    -    payload_fd = payload.clone
    -    output = ""
    -    loop do
    -      data = payload_fd.read(16384, buffer)
    -      break if data.nil? # listerextractor.write(data)
    -      lister.write(data)
    -
    -      # Read output from the pipe.
    -      begin
    -        output << lister.read_nonblock(16384)
    -      rescue Errno::EAGAIN
    -        # Nothing to read, move on!
    -      end
    -    end
    -    lister.close_write
    -
    -    # Read remaining output
    -    begin
    -      output << lister.read
    -    rescue Errno::EAGAIN
    -      # Because read_nonblock enables NONBLOCK the 'lister' fd,
    -      # and we may have invoked a read *before* cpio has started
    -      # writing, let's keep retrying this read until we get an EOF
    -      retry
    -    rescue EOFError
    -      # At EOF, hurray! We're done reading.
    -    end
    -
    -    # Split output by newline and strip leading "."
    -    @files = output.split("\n").collect { |s| s.gsub(/^\./, "") }
    -    return @files
    -  ensure
    -    lister.close unless lister.nil?
    -    payload_fd.close unless payload_fd.nil?
    +    # RPM stores the file metadata split across multiple tags.
    +    # A single path's filename (with no directories) is stored in the "basename" tag.
    +    # The directory a file lives in is stored in the "dirnames" tag
    +    #
    +    # We can join each entry of dirnames and basenames to make the full filename.
    +    return tags[:dirnames].zip(tags[:basenames]).map &File.method(:join)
       end # def files
     
       def mask?(value, mask)
    
  • lib/arr-pm/file.rb+6 43 modified
    @@ -195,49 +195,12 @@ def config_files
       # 
       #   % rpm2cpio blah.rpm | cpio -it
       def files
    -    return @files unless @files.nil?
    -
    -    lister = IO.popen("#{tags[:payloadcompressor]} -d | cpio -it --quiet", "r+")
    -    buffer = ""
    -    begin
    -        buffer.force_encoding("BINARY")
    -    rescue NoMethodError
    -        # Do Nothing
    -    end
    -    payload_fd = payload.clone
    -    output = ""
    -    loop do
    -      data = payload_fd.read(16384, buffer)
    -      break if data.nil? # listerextractor.write(data)
    -      lister.write(data)
    -
    -      # Read output from the pipe.
    -      begin
    -        output << lister.read_nonblock(16384)
    -      rescue Errno::EAGAIN
    -        # Nothing to read, move on!
    -      end
    -    end
    -    lister.close_write
    -
    -    # Read remaining output
    -    begin
    -      output << lister.read
    -    rescue Errno::EAGAIN
    -      # Because read_nonblock enables NONBLOCK the 'lister' fd,
    -      # and we may have invoked a read *before* cpio has started
    -      # writing, let's keep retrying this read until we get an EOF
    -      retry
    -    rescue EOFError
    -      # At EOF, hurray! We're done reading.
    -    end
    -
    -    # Split output by newline and strip leading "."
    -    @files = output.split("\n").collect { |s| s.gsub(/^\./, "") }
    -    return @files
    -  ensure
    -    lister.close unless lister.nil?
    -    payload_fd.close unless payload_fd.nil?
    +    # RPM stores the file metadata split across multiple tags.
    +    # A single path's filename (with no directories) is stored in the "basename" tag.
    +    # The directory a file lives in is stored in the "dirnames" tag
    +    #
    +    # We can join each entry of dirnames and basenames to make the full filename.
    +    return tags[:dirnames].zip(tags[:basenames]).map &File.method(:join)
       end # def files
     
       def mask?(value, mask)
    
  • spec/fixtures/pagure-mirror-5.13.2-5.fc35.noarch.rpm+0 0 added
  • spec/fixtures/pagure-mirror-5.13.2-5.fc35.noarch.rpm+0 0 added
  • spec/rpm/file_spec.rb+22 0 added
    @@ -0,0 +1,22 @@
    +# encoding: utf-8
    +
    +require "arr-pm/file"
    +
    +describe ::RPM::File do
    +  context "with a known good rpm" do
    +    let(:path) { File.join(File.dirname(__FILE__), "../fixtures/pagure-mirror-5.13.2-5.fc35.noarch.rpm") }
    +    subject { described_class.new(path) }
    +
    +    context "#files" do
    +      let(:files) { [
    +        "/usr/lib/systemd/system/pagure_mirror.service",
    +        "/usr/share/licenses/pagure-mirror",
    +        "/usr/share/licenses/pagure-mirror/LICENSE"
    +      ]}
    +
    +      it "should have the correct list of files" do
    +        expect(subject.files).to eq(files)
    +      end
    +    end
    +  end
    +end
    
  • spec/rpm/file_spec.rb+22 0 added
    @@ -0,0 +1,22 @@
    +# encoding: utf-8
    +
    +require "arr-pm/file"
    +
    +describe ::RPM::File do
    +  context "with a known good rpm" do
    +    let(:path) { File.join(File.dirname(__FILE__), "../fixtures/pagure-mirror-5.13.2-5.fc35.noarch.rpm") }
    +    subject { described_class.new(path) }
    +
    +    context "#files" do
    +      let(:files) { [
    +        "/usr/lib/systemd/system/pagure_mirror.service",
    +        "/usr/share/licenses/pagure-mirror",
    +        "/usr/share/licenses/pagure-mirror/LICENSE"
    +      ]}
    +
    +      it "should have the correct list of files" do
    +        expect(subject.files).to eq(files)
    +      end
    +    end
    +  end
    +end
    

Vulnerability mechanics

Root cause

"The `extract` and `files` methods pass the untrusted `payloadcompressor` tag value directly into `IO.popen`, enabling OS command injection if the value contains shell metacharacters."

Attack vector

An attacker crafts a malicious RPM whose `payloadcompressor` header field contains shell metacharacters (e.g., `some invalid | string`). When a victim calls `RPM::File#extract` or `RPM::File#files`, the library passes this value unsanitized into `IO.popen`, which spawns a shell and executes the injected command. No authentication is required; the attacker only needs to supply the crafted RPM file to the victim's application.

Affected code

The vulnerable code is in `lib/arr-pm/file.rb` in the `extract` method (line using `IO.popen("#{tags[:payloadcompressor]} -d | ...")`) and the `files` method (line using `IO.popen("#{tags[:payloadcompressor]} -d | cpio -it ...")`). Both methods pass the untrusted `tags[:payloadcompressor]` value directly into a shell command without validation.

What the fix does

Patch [patch_id=1641355] adds a `valid_compressor?` method that checks the `payloadcompressor` value against a whitelist of known compressors (`gzip`, `bzip2`, `xz`, `lzma`, `zstd`). If the value is not in the whitelist, `extract` raises a `RuntimeError` before reaching `IO.popen`. Patch [patch_id=1641353] eliminates the command injection entirely in the `files` method by replacing the `IO.popen`/`cpio` pipeline with a safe join of RPM header tags (`dirnames` and `basenames`), removing the need to invoke any external decompressor.

Preconditions

  • inputAttacker must supply a crafted RPM file with a malicious payloadcompressor tag value.
  • networkNo special network position required; the victim must process the attacker-controlled RPM.

Generated on May 23, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.