CVE-2023-51763 (published 2023-12-24T04:15Z)

csv_builder.rb in ActiveAdmin (aka Active Admin) before 3.2.0 allows CSV injection.

CVSS v3 base score: 9.8, CRITICAL (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-1236
cwe_name: Improper Neutralization of Formula Elements in a CSV File
description: The product saves user-provided information into a Comma-Separated Value (CSV) file, but it does not neutralize or incorrectly neutralizes special elements that could be interpreted as a command when the file is opened by a spreadsheet product.
extended_description: User-provided data is often saved to traditional databases.  This data can be exported to a CSV file, which allows users to read the data using spreadsheet software such as Excel, Numbers, or Calc.  This software interprets entries beginning with '=' as formulas, which are then executed by the spreadsheet software.  The software's formula language often allows methods to access hyperlinks or the local command line, and frequently allows enough characters to invoke an entire script. Attackers can populate data fields which, when saved to a CSV file, may attempt information exfiltration or other malicious activity when automatically executed by the spreadsheet software.
			

Fix information:
repo_url: https://github.com/activeadmin/activeadmin
commit_details: repo_url: https://github.com/activeadmin/activeadmin
author: Matias Grunberg
author_date: 2023-12-09 22:38:32-03:00
msg: Protect against CSV Injection (#8161)

protect against CSV Injection

Read more here https://owasp.org/www-community/attacks/CSV_Injection
repository: repo_url: https://github.com/activeadmin/activeadmin
repo_name: activeadmin/activeadmin
description: The administration framework for Ruby on Rails applications.
repo_language: Ruby
owner: activeadmin
file_changes: filename: csv_builder.rb
change_type: ModificationType.MODIFY
diff: @@ -51,7 +51,7 @@ module ActiveAdmin
       csv << bom if bom
 
       if column_names
-        csv << CSV.generate_line(columns.map { |c| encode c.name, options }, **csv_options)
+        csv << CSV.generate_line(columns.map { |c| sanitize(encode(c.name, options)) }, **csv_options)
       end
 
       controller.send(:in_paginated_batches) do |resource|
@@ -70,7 +70,7 @@ module ActiveAdmin
 
     def build_row(resource, columns, options)
       columns.map do |column|
-        encode call_method_or_proc_on(resource, column.data), options
+        sanitize(encode(call_method_or_proc_on(resource, column.data), options))
       end
     end
 
@@ -86,6 +86,10 @@ module ActiveAdmin
       end
     end
 
+    def sanitize(content)
+      Sanitizer.sanitize(content)
+    end
+
     def method_missing(method, *args, &block)
       if @view_context.respond_to? method
         @view_context.public_send method, *args, &block
@@ -120,4 +124,21 @@ module ActiveAdmin
       @column_transitive_options ||= @options.slice(*COLUMN_TRANSITIVE_OPTIONS)
     end
   end
+
+  # Prevents CSV Injection according to https://owasp.org/www-community/attacks/CSV_Injection
+  module Sanitizer
+    extend self
+
+    ATTACK_CHARACTERS = ['=', '+', '-', '@', "\t", "\r"].freeze
+
+    def sanitize(value)
+      return "'#{value}" if require_sanitization?(value)
+
+      value
+    end
+
+    def require_sanitization?(value)
+      value.is_a?(String) && value.starts_with?(*ATTACK_CHARACTERS)
+    end
+  end
 end

programming_language: Ruby
filename: csv_builder_spec.rb
change_type: ModificationType.MODIFY
diff: @@ -277,4 +277,52 @@ RSpec.describe ActiveAdmin::CSVBuilder do
       end
     end
   end
+
+  context 'csv injection' do
+    let(:dummy_controller) do
+      class DummyController
+        def in_paginated_batches(&block)
+          Post.all.each(&block)
+        end
+
+        def view_context
+          MethodOrProcHelper
+        end
+      end
+      DummyController.new
+    end
+
+    let(:builder) do
+      ActiveAdmin::CSVBuilder.new do
+        column(:id)
+        column(:title)
+      end
+    end
+
+    ['=', '+', '-', '@', "\t", "\r"].each do |char|
+      it "prepends a single quote when column starts with a #{char} character" do
+        attack = "#{char}1+2"
+
+        escaped_attack = "'#{attack}"
+        escaped_attack = "\"#{escaped_attack}\"" if char == "\r"
+
+        post = Post.create!(title: attack)
+        receiver = []
+        builder.build dummy_controller, receiver
+        line = receiver.last
+        expect(line).to eq "#{post.id},#{escaped_attack}\n"
+      end
+
+      it "accounts for the field separator when character #{char} is used to inject a formula" do
+        attack = "#{char}1+2'\" ;,#{char}1+2"
+        escaped_attack = "\"'#{attack.gsub('"', '""')}\""
+
+        post = Post.create!(title: attack)
+        receiver = []
+        builder.build dummy_controller, receiver
+        line = receiver.last
+        expect(line).to eq "#{post.id},#{escaped_attack}\n"
+      end
+    end
+  end
 end

programming_language: Ruby