summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGabriel Morcote <[email protected]>2019-12-20 15:01:35 -0600
committerStefan Daschek <[email protected]>2019-12-20 22:01:35 +0100
commit0a223011a26949ddc00eba882005daee7afeb6a6 (patch)
treea5ffc3459f43e0efef8326dbd1738cac5c5dbc0f
parent99d3d2fbe5b07aa005b475b8cdc4e6238c3d5e28 (diff)
downloadcaxlsx-0a223011a26949ddc00eba882005daee7afeb6a6.tar.gz
caxlsx-0a223011a26949ddc00eba882005daee7afeb6a6.zip
Add option to protect against formula injection attacks (#34)
Caxlsx used to treat cell values beginning with an equal sign as formula by default. This can be dangerous if the input data is user generated or coming from other untrusted sources (see https://www.owasp.org/index.php/CSV_Injection for details). This commit adds a new option `escape_formulas` that can be used with `#add_row` and on instances of `Cell`. If set to true, cell values beginning with an equal sign are treated as normal strings (and will be displayed literally by Excel and co.)
-rwxr-xr-xexamples/example.rb17
-rw-r--r--lib/axlsx/workbook/worksheet/cell.rb20
-rw-r--r--lib/axlsx/workbook/worksheet/row.rb3
-rw-r--r--lib/axlsx/workbook/worksheet/worksheet.rb10
-rw-r--r--test/workbook/worksheet/tc_cell.rb30
-rw-r--r--test/workbook/worksheet/tc_row.rb17
6 files changed, 94 insertions, 3 deletions
diff --git a/examples/example.rb b/examples/example.rb
index 9266f830..8fcfe7d4 100755
--- a/examples/example.rb
+++ b/examples/example.rb
@@ -21,6 +21,7 @@ examples << :images
examples << :format_dates
examples << :mbcs
examples << :formula
+examples << :escape_formulas
examples << :auto_filter
examples << :sheet_protection
examples << :data_types
@@ -364,6 +365,21 @@ if examples.include? :formula
end
##```
+##Escaping formulas for cells
+#```ruby
+if examples.include? :escape_formulas
+ wb.add_worksheet(:name => "Escaping Formulas") do |sheet|
+ sheet.add_row [1, 2, 3, "=SUM(A2:C2)"], escape_formulas: true
+ sheet.add_row [
+ "=IF(2+2=4,4,5)",
+ "=IF(13+13=4,4,5)",
+ "=IF(99+99=4,4,5)"
+ ], escape_formulas: [true, false, true]
+ end
+ p.serialize("escaped_formulas.xlsx")
+end
+##```
+
##Auto Filter
#```ruby
@@ -882,4 +898,3 @@ if examples.include? :tab_color
p.serialize 'tab_color.xlsx'
end
##```
-
diff --git a/lib/axlsx/workbook/worksheet/cell.rb b/lib/axlsx/workbook/worksheet/cell.rb
index 99b3f8b0..53bea28b 100644
--- a/lib/axlsx/workbook/worksheet/cell.rb
+++ b/lib/axlsx/workbook/worksheet/cell.rb
@@ -30,6 +30,10 @@ module Axlsx
# @option options [String] color an 8 letter rgb specification
# @option options [Number] formula_value The value to cache for a formula cell.
# @option options [Symbol] scheme must be one of :none, major, :minor
+ # @option options [Boolean] escape_formulas - Whether to treat a value starting with an equal
+ # sign as formula (default) or as simple string.
+ # Allowing user generated data to be interpreted as formulas can be dangerous
+ # (see https://www.owasp.org/index.php/CSV_Injection for details).
def initialize(row, value = nil, options = {})
@row = row
# Do not use instance vars if not needed to use less RAM
@@ -38,6 +42,8 @@ module Axlsx
type = options.delete(:type) || cell_type_from_value(value)
self.type = type unless type == :string
+ escape_formulas = options[:escape_formulas]
+ self.escape_formulas = escape_formulas unless escape_formulas.nil?
val = options.delete(:style)
self.style = val unless val.nil? || val == 0
@@ -102,6 +108,18 @@ module Axlsx
self.value = @value unless !defined?(@value) || @value.nil?
end
+ # Whether to treat a value starting with an equal
+ # sign as formula (default) or as simple string.
+ # Allowing user generated data to be interpreted as formulas can be dangerous
+ # (see https://www.owasp.org/index.php/CSV_Injection for details).
+ # @return [Boolean]
+ attr_reader :escape_formulas
+
+ def escape_formulas=(v)
+ Axlsx.validate_boolean(v)
+ @escape_formulas = v
+ end
+
# The value of this cell.
# @return [String, Integer, Float, Time, Boolean] casted value based on cell's type attribute.
attr_reader :value
@@ -324,6 +342,8 @@ module Axlsx
end
def is_formula?
+ return false if escape_formulas
+
type == :string && @value.to_s.start_with?(?=)
end
diff --git a/lib/axlsx/workbook/worksheet/row.rb b/lib/axlsx/workbook/worksheet/row.rb
index b394279f..decd27c7 100644
--- a/lib/axlsx/workbook/worksheet/row.rb
+++ b/lib/axlsx/workbook/worksheet/row.rb
@@ -147,10 +147,11 @@ module Axlsx
# @option options [Array, Integer] style
def array_to_cells(values, options={})
DataTypeValidator.validate :array_to_cells, Array, values
- types, style, formula_values = options.delete(:types), options.delete(:style), options.delete(:formula_values)
+ types, style, formula_values, escape_formulas = options.delete(:types), options.delete(:style), options.delete(:formula_values), options.delete(:escape_formulas)
values.each_with_index do |value, index|
options[:style] = style.is_a?(Array) ? style[index] : style if style
options[:type] = types.is_a?(Array) ? types[index] : types if types
+ options[:escape_formulas] = escape_formulas.is_a?(Array) ? escape_formulas[index] : escape_formulas if escape_formulas
options[:formula_value] = formula_values[index] if formula_values.is_a?(Array)
self[index] = Cell.new(self, value, options)
diff --git a/lib/axlsx/workbook/worksheet/worksheet.rb b/lib/axlsx/workbook/worksheet/worksheet.rb
index 9cfefa51..4fb95d4c 100644
--- a/lib/axlsx/workbook/worksheet/worksheet.rb
+++ b/lib/axlsx/workbook/worksheet/worksheet.rb
@@ -390,6 +390,12 @@ module Axlsx
# @example - use << alias
# ws << [3, 4, 5], :types => [nil, :float]
#
+ # @example - specify whether a row should escape formulas or not
+ # ws.add_row ['=IF(2+2=4,4,5)', 2, 3], :escape_formulas=>true
+ #
+ # @example - specify whether a certain cells in a row should escape formulas or not
+ # ws.add_row ['=IF(2+2=4,4,5)', '=IF(13+13=4,4,5)'], :escape_formulas=>[true, false]
+ #
# @see Worksheet#column_widths
# @return [Row]
# @option options [Array] values
@@ -397,6 +403,10 @@ module Axlsx
# @option options [Array, Integer] style
# @option options [Array] widths each member of the widths array will affect how auto_fit behavies.
# @option options [Float] height the row's height (in points)
+ # @option options [Array, Boolean] escape_formulas - Whether to treat a value starting with an equal
+ # sign as formula (default) or as simple string.
+ # Allowing user generated data to be interpreted as formulas can be dangerous
+ # (see https://www.owasp.org/index.php/CSV_Injection for details).
def add_row(values=[], options={})
row = Row.new(self, values, options)
update_column_info row, options.delete(:widths)
diff --git a/test/workbook/worksheet/tc_cell.rb b/test/workbook/worksheet/tc_cell.rb
index 11b54f44..d3ca9fe8 100644
--- a/test/workbook/worksheet/tc_cell.rb
+++ b/test/workbook/worksheet/tc_cell.rb
@@ -8,7 +8,7 @@ class TestCell < Test::Unit::TestCase
@ws = p.workbook.add_worksheet :name=>"hmmm"
p.workbook.styles.add_style :sz=>20
@row = @ws.add_row
- @c = @row.add_cell 1, :type=>:float, :style=>1
+ @c = @row.add_cell 1, :type=>:float, :style=>1, :escape_formulas=>true
data = (0..26).map { |index| index }
@ws.add_row data
@cAA = @ws["AA2"]
@@ -19,6 +19,7 @@ class TestCell < Test::Unit::TestCase
assert_equal(@c.type, :float, "type option is applied")
assert_equal(@c.style, 1, "style option is applied")
assert_equal(@c.value, 1.0, "type option is applied and value is casted")
+ assert_equal(@c.escape_formulas, true, "escape formulas option is applied")
end
def test_style_date_data
@@ -321,6 +322,33 @@ class TestCell < Test::Unit::TestCase
assert(doc.xpath("//f[text()='IF(2+2=4,4,5)']").any?)
end
+ def test_to_xml_string_formula_escaped
+ p = Axlsx::Package.new
+ ws = p.workbook.add_worksheet do |sheet|
+ sheet.add_row ["=IF(2+2=4,4,5)"], escape_formulas: true
+ end
+ doc = Nokogiri::XML(ws.to_xml_string)
+ doc.remove_namespaces!
+ assert(doc.xpath("//t[text()='=IF(2+2=4,4,5)']").any?)
+ end
+
+ def test_to_xml_string_formula_escape_array_parameter
+ p = Axlsx::Package.new
+ ws = p.workbook.add_worksheet do |sheet|
+ sheet.add_row [
+ "=IF(2+2=4,4,5)",
+ "=IF(13+13=4,4,5)",
+ "=IF(99+99=4,4,5)"
+ ], escape_formulas: [true, false, true]
+ end
+ doc = Nokogiri::XML(ws.to_xml_string)
+ doc.remove_namespaces!
+
+ assert(doc.xpath("//t[text()='=IF(2+2=4,4,5)']").any?)
+ assert(doc.xpath("//f[text()='IF(13+13=4,4,5)']").any?)
+ assert(doc.xpath("//t[text()='=IF(99+99=4,4,5)']").any?)
+ end
+
def test_to_xml_string_array_formula
p = Axlsx::Package.new
ws = p.workbook.add_worksheet do |sheet|
diff --git a/test/workbook/worksheet/tc_row.rb b/test/workbook/worksheet/tc_row.rb
index 43e7d23f..f909129c 100644
--- a/test/workbook/worksheet/tc_row.rb
+++ b/test/workbook/worksheet/tc_row.rb
@@ -60,6 +60,23 @@ class TestRow < Test::Unit::TestCase
end
+ def test_array_to_cells_with_escape_formulas
+ row = ['=HYPERLINK("http://www.example.com", "CSV Payload")', '=Bar']
+ @ws.add_row row, escape_formulas: true
+
+ assert_equal @ws.rows.last.cells[0].escape_formulas, true
+ assert_equal @ws.rows.last.cells[1].escape_formulas, true
+ end
+
+ def test_array_to_cells_with_escape_formulas_as_an_array
+ row = ['=HYPERLINK("http://www.example.com", "CSV Payload")', '+Foo', '-Bar']
+ @ws.add_row row, escape_formulas: [true, false, true]
+
+ assert_equal @ws.rows.last.cells.first.escape_formulas, true
+ assert_equal @ws.rows.last.cells[1].escape_formulas, false
+ assert_equal @ws.rows.last.cells[2].escape_formulas, true
+ end
+
def test_custom_height
@row.height = 20
assert(@row.custom_height)