streamy_csv 0.4.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/streamy_csv/injection_sanitizer.rb +18 -0
- data/lib/streamy_csv/version.rb +1 -1
- data/lib/streamy_csv.rb +9 -24
- data/spec/lib/streamy_csv/injection_sanitizer_spec.rb +39 -0
- data/spec/streamy_csv_spec.rb +32 -26
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d675a642b3b39ce706e4f8e5ec0cd67809db6f43
|
4
|
+
data.tar.gz: 6a68f7897d3070e18e45f02200685bbc4c3c2d89
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6cf3a9f43c25f5a077453be47c80f46d75e2265fbac842274f5a11316533c1628fe6e09dd77af76b394d61ed7b1b0736366111a1704c5ef41acd3fb71219a587
|
7
|
+
data.tar.gz: 52f04bddb8b2d850fc5a3320eac6e8afde8c807b98314661c8818f9d483473ede3303064d41bb3866474b44625a0bbdb8912a76ec0730a967bd0c442cc9826c5
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module StreamyCsv
|
2
|
+
class InjectionSanitizer
|
3
|
+
PREFIXES_TO_ESCAPE=%w(= @ + - |)
|
4
|
+
ESCAPE_CHAR="'"
|
5
|
+
|
6
|
+
def self.sanitize_csv_row(row)
|
7
|
+
if row.is_a?(CSV::Row)
|
8
|
+
sanitized_row = row.dup
|
9
|
+
row.each do |title, value|
|
10
|
+
if value.to_s.start_with?(*PREFIXES_TO_ESCAPE)
|
11
|
+
sanitized_row[title] = "#{ESCAPE_CHAR}#{value}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
row
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/streamy_csv/version.rb
CHANGED
data/lib/streamy_csv.rb
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
require "streamy_csv/version"
|
2
|
+
require "streamy_csv/injection_sanitizer"
|
2
3
|
|
3
4
|
module StreamyCsv
|
4
|
-
CSV_OPERATORS = ['+','-','=','@','%']
|
5
|
-
UNESCAPED_PIPES_RGX = /(?<!\\)(?:\\{2})*\K\|/
|
6
5
|
|
7
6
|
# stream_csv('data.csv', MyModel.header_row) do |rows|
|
8
7
|
# MyModel.find_each do |my_model|
|
@@ -12,7 +11,7 @@ module StreamyCsv
|
|
12
11
|
#
|
13
12
|
#
|
14
13
|
|
15
|
-
def stream_csv(file_name, header_row, sanitize
|
14
|
+
def stream_csv(file_name, header_row, sanitize=true, &block)
|
16
15
|
set_streaming_headers
|
17
16
|
set_file_headers(file_name)
|
18
17
|
|
@@ -30,30 +29,16 @@ module StreamyCsv
|
|
30
29
|
end
|
31
30
|
|
32
31
|
def csv_lines(header_row, sanitize, &block)
|
33
|
-
Enumerator.new do |
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
yielder.yield row
|
32
|
+
Enumerator.new do |rows|
|
33
|
+
if sanitize
|
34
|
+
def rows.<<(row)
|
35
|
+
super StreamyCsv::InjectionSanitizer.sanitize_csv_row(row).to_s
|
36
|
+
end
|
39
37
|
end
|
38
|
+
rows << header_row if header_row
|
39
|
+
block.call(rows)
|
40
40
|
end
|
41
|
-
end
|
42
|
-
|
43
|
-
def appendHeader(rows, header_row, sanitize)
|
44
|
-
if header_row && header_row.any?
|
45
|
-
sanitize! header_row if sanitize
|
46
|
-
rows << header_row.to_s
|
47
|
-
end
|
48
|
-
rows
|
49
|
-
end
|
50
41
|
|
51
|
-
def sanitize!(enumerable)
|
52
|
-
return unless enumerable && enumerable.is_a?(Enumerable)
|
53
|
-
enumerable = enumerable.fields if enumerable.is_a?(CSV::Row)
|
54
|
-
enumerable.each do |field|
|
55
|
-
field.gsub!(UNESCAPED_PIPES_RGX,'\|') if field.is_a?(String) && field.start_with?(*CSV_OPERATORS)
|
56
|
-
end
|
57
42
|
end
|
58
43
|
|
59
44
|
def set_file_headers(file_name)
|
@@ -0,0 +1,39 @@
|
|
1
|
+
$: << File.join(File.dirname(__FILE__), "../../../lib")
|
2
|
+
require 'streamy_csv/injection_sanitizer'
|
3
|
+
require 'csv'
|
4
|
+
|
5
|
+
describe StreamyCsv::InjectionSanitizer do
|
6
|
+
describe 'sanitize_csv_row' do
|
7
|
+
it "returns the data as is in case it's not a csv row" do
|
8
|
+
StreamyCsv::InjectionSanitizer.sanitize_csv_row(10).should == 10
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'escapes the leading escape leading | character on any column' do
|
12
|
+
row = CSV::Row.new([:x], ["|RAND()"])
|
13
|
+
sanitized_row = StreamyCsv::InjectionSanitizer.sanitize_csv_row(row)
|
14
|
+
sanitized_row.to_s.strip.should == "'|RAND()"
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'escapes the leading escape leading + character on any column' do
|
18
|
+
row = CSV::Row.new([:x], ["-RAND()"])
|
19
|
+
sanitized_row = StreamyCsv::InjectionSanitizer.sanitize_csv_row(row)
|
20
|
+
sanitized_row.to_s.strip.should == "'-RAND()"
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'escapes the leading escape leading + character on any column' do
|
24
|
+
row = CSV::Row.new([:x], ["+RAND()"])
|
25
|
+
sanitized_row = StreamyCsv::InjectionSanitizer.sanitize_csv_row(row)
|
26
|
+
sanitized_row.to_s.strip.should == "'+RAND()"
|
27
|
+
end
|
28
|
+
it 'escapes the leading escape leading @ character on any column' do
|
29
|
+
row = CSV::Row.new([:x], ["@RAND()"])
|
30
|
+
sanitized_row = StreamyCsv::InjectionSanitizer.sanitize_csv_row(row)
|
31
|
+
sanitized_row.to_s.strip.should == "'@RAND()"
|
32
|
+
end
|
33
|
+
it 'escapes the leading escape leading = character on any column' do
|
34
|
+
row = CSV::Row.new([:x], ["=RAND()"])
|
35
|
+
sanitized_row = StreamyCsv::InjectionSanitizer.sanitize_csv_row(row)
|
36
|
+
sanitized_row.to_s.strip.should == "'=RAND()"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/spec/streamy_csv_spec.rb
CHANGED
@@ -34,14 +34,14 @@ describe StreamyCsv do
|
|
34
34
|
it 'sets the streaming headers' do
|
35
35
|
@controller.stream_csv('abc.csv', @header)
|
36
36
|
@controller.headers.should include({'X-Accel-Buffering' => 'no',
|
37
|
-
|
37
|
+
"Cache-Control" => "no-cache"
|
38
38
|
})
|
39
39
|
end
|
40
40
|
|
41
41
|
it 'sets the file headers' do
|
42
42
|
@controller.stream_csv('abc.csv', @header)
|
43
43
|
@controller.headers.should include({"Content-Type" => "text/csv",
|
44
|
-
|
44
|
+
"Content-disposition" => "attachment; filename=\"abc.csv\""
|
45
45
|
})
|
46
46
|
end
|
47
47
|
|
@@ -59,40 +59,46 @@ describe StreamyCsv do
|
|
59
59
|
@controller.response.status.should == 200
|
60
60
|
@controller.response_body.is_a?(Enumerator).should == true
|
61
61
|
end
|
62
|
+
|
62
63
|
it 'sanitizes header and contents and streams the csv file' do
|
63
|
-
|
64
|
-
row_2 = CSV::Row.new([:name, :title], ["=cmd|' /C", 'Pres'])
|
65
|
-
header = [:name, "=cmd|' /C"]
|
66
|
-
rows = [header, row_1]
|
64
|
+
header = CSV::Row.new([:name, :title], ['Name', "=cmd|' /C"], true)
|
67
65
|
|
68
|
-
@controller.stream_csv('abc.csv',
|
69
|
-
rows <<
|
70
|
-
rows <<
|
66
|
+
@controller.stream_csv('abc.csv', header) do |rows|
|
67
|
+
rows << CSV::Row.new([:name, :title], ['AB', 'Mr'])
|
68
|
+
rows << CSV::Row.new([:name, :title], ["=cmd|' /C", '-Pres'])
|
69
|
+
rows << CSV::Row.new([:name, :title], ["@something", '+Pres'])
|
71
70
|
end
|
71
|
+
|
72
72
|
@controller.response.status.should == 200
|
73
73
|
@controller.response_body.is_a?(Enumerator).should == true
|
74
|
-
@controller.response_body.
|
75
|
-
|
76
|
-
|
77
|
-
|
74
|
+
body = @controller.response_body.to_a
|
75
|
+
body.size.should == 4
|
76
|
+
|
77
|
+
body[0].to_s.strip.should == "Name,'=cmd|' /C"
|
78
|
+
body[1].to_s.strip.should == "AB,Mr"
|
79
|
+
body[2].to_s.strip.should == "'=cmd|' /C,'-Pres"
|
80
|
+
body[3].to_s.strip.should == "'@something,'+Pres"
|
78
81
|
end
|
79
|
-
it 'does not sanitize the csv if the option provided' do
|
80
|
-
row_1 = CSV::Row.new([:name, :title], ['AB', 'Mr'])
|
81
|
-
row_2 = CSV::Row.new([:name, :title], ["=cmd|' /C", 'Pres'])
|
82
|
-
header = [:name, "=cmd|' /C"]
|
83
|
-
rows = [header, row_1]
|
84
82
|
|
85
|
-
|
86
|
-
|
87
|
-
|
83
|
+
it 'does not sanitize header and contents if specified and streams the csv file' do
|
84
|
+
header = CSV::Row.new([:name, :title], ['Name', "=cmd|' /C"], true)
|
85
|
+
|
86
|
+
@controller.stream_csv('abc.csv', header, false) do |rows|
|
87
|
+
rows << CSV::Row.new([:name, :title], ['AB', 'Mr'])
|
88
|
+
rows << CSV::Row.new([:name, :title], ["=cmd|' /C", '-Pres'])
|
89
|
+
rows << CSV::Row.new([:name, :title], ["@something", '+Pres'])
|
88
90
|
end
|
91
|
+
|
89
92
|
@controller.response.status.should == 200
|
90
93
|
@controller.response_body.is_a?(Enumerator).should == true
|
91
|
-
@controller.response_body.
|
92
|
-
|
93
|
-
|
94
|
-
|
94
|
+
body = @controller.response_body.to_a
|
95
|
+
body.size.should == 4
|
96
|
+
|
97
|
+
body[0].to_s.strip.should == "Name,=cmd|' /C"
|
98
|
+
body[1].to_s.strip.should == "AB,Mr"
|
99
|
+
body[2].to_s.strip.should == "=cmd|' /C,-Pres"
|
100
|
+
body[3].to_s.strip.should == "@something,+Pres"
|
95
101
|
end
|
96
102
|
end
|
97
103
|
|
98
|
-
end
|
104
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: streamy_csv
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- smsohan
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-09-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
@@ -39,7 +39,9 @@ files:
|
|
39
39
|
- README.md
|
40
40
|
- Rakefile
|
41
41
|
- lib/streamy_csv.rb
|
42
|
+
- lib/streamy_csv/injection_sanitizer.rb
|
42
43
|
- lib/streamy_csv/version.rb
|
44
|
+
- spec/lib/streamy_csv/injection_sanitizer_spec.rb
|
43
45
|
- spec/streamy_csv_spec.rb
|
44
46
|
- streamy_csv.gemspec
|
45
47
|
homepage: https://github.com/smsohan/streamy_csv
|
@@ -61,10 +63,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
61
63
|
version: '0'
|
62
64
|
requirements: []
|
63
65
|
rubyforge_project:
|
64
|
-
rubygems_version: 2.
|
66
|
+
rubygems_version: 2.5.2.1
|
65
67
|
signing_key:
|
66
68
|
specification_version: 4
|
67
69
|
summary: Provides a simple API for your controllers to stream CSV files one row at
|
68
70
|
a time
|
69
71
|
test_files:
|
72
|
+
- spec/lib/streamy_csv/injection_sanitizer_spec.rb
|
70
73
|
- spec/streamy_csv_spec.rb
|