jekyll-minifier 0.1.10 → 0.2.1

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.
@@ -0,0 +1,306 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Jekyll Minifier - ReDoS Security Protection" do
4
+ let(:overrides) { Hash.new }
5
+ let(:config) do
6
+ Jekyll.configuration(Jekyll::Utils.deep_merge_hashes({
7
+ "full_rebuild" => true,
8
+ "source" => source_dir,
9
+ "destination" => dest_dir,
10
+ "show_drafts" => true,
11
+ "url" => "http://example.org",
12
+ "name" => "My awesome site"
13
+ }, overrides))
14
+ end
15
+ let(:site) { Jekyll::Site.new(config) }
16
+ let(:compressor) { Jekyll::Document.new(source_dir("_posts/2014-03-01-test-review-1.md"), site: site, collection: site.collections["posts"]) }
17
+
18
+ before(:each) do
19
+ allow(ENV).to receive(:[]).and_call_original
20
+ allow(ENV).to receive(:[]).with('JEKYLL_ENV').and_return('production')
21
+ end
22
+
23
+ describe "ReDoS Attack Prevention" do
24
+ context "with safe preserve patterns" do
25
+ let(:overrides) do
26
+ {
27
+ "jekyll-minifier" => {
28
+ "preserve_patterns" => [
29
+ "<!-- PRESERVE -->.*?<!-- /PRESERVE -->",
30
+ "<script[^>]*>.*?</script>",
31
+ "<style[^>]*>.*?</style>"
32
+ ]
33
+ }
34
+ }
35
+ end
36
+
37
+ it "processes safe patterns without issues" do
38
+ expect { site.process }.not_to raise_error
39
+ expect(File.exist?(dest_dir("index.html"))).to be true
40
+ end
41
+
42
+ it "compiles safe patterns successfully" do
43
+ patterns = compressor.send(:compile_preserve_patterns, [
44
+ "<!-- PRESERVE -->.*?<!-- /PRESERVE -->",
45
+ "<script[^>]*>.*?</script>"
46
+ ])
47
+
48
+ expect(patterns.length).to eq(2)
49
+ expect(patterns.all? { |p| p.is_a?(Regexp) }).to be true
50
+ end
51
+ end
52
+
53
+ context "with potentially dangerous ReDoS patterns" do
54
+ let(:dangerous_patterns) do
55
+ [
56
+ # Nested quantifiers - classic ReDoS vector
57
+ "(a+)+b",
58
+ "(a*)*b",
59
+ "(a+)*b",
60
+ "(a*)+b",
61
+
62
+ # Alternation with overlapping patterns
63
+ "(a|a)*b",
64
+ "(ab|ab)*c",
65
+ "(.*|.*)*d",
66
+
67
+ # Excessively long patterns
68
+ "a" * 1001,
69
+
70
+ # Complex nested structures
71
+ "(" * 15 + "a" + ")" * 15,
72
+
73
+ # Excessive quantifiers
74
+ "a+" * 25 + "b"
75
+ ]
76
+ end
77
+
78
+ it "rejects dangerous ReDoS patterns gracefully" do
79
+ # Should not raise errors, but should warn and skip dangerous patterns
80
+ expect(Jekyll.logger).to receive(:warn).at_least(:once)
81
+
82
+ patterns = compressor.send(:compile_preserve_patterns, dangerous_patterns)
83
+
84
+ # All dangerous patterns should be filtered out
85
+ expect(patterns.length).to eq(0)
86
+ end
87
+
88
+ it "continues processing when dangerous patterns are present" do
89
+ overrides = {
90
+ "jekyll-minifier" => {
91
+ "preserve_patterns" => dangerous_patterns
92
+ }
93
+ }
94
+
95
+ test_site = Jekyll::Site.new(Jekyll.configuration({
96
+ "full_rebuild" => true,
97
+ "source" => source_dir,
98
+ "destination" => dest_dir,
99
+ "show_drafts" => true,
100
+ "jekyll-minifier" => {
101
+ "preserve_patterns" => dangerous_patterns
102
+ }
103
+ }))
104
+
105
+ # Should complete processing despite dangerous patterns
106
+ expect { test_site.process }.not_to raise_error
107
+ expect(File.exist?(dest_dir("index.html"))).to be true
108
+ end
109
+ end
110
+
111
+ context "with mixed safe and dangerous patterns" do
112
+ let(:mixed_patterns) do
113
+ [
114
+ "<!-- PRESERVE -->.*?<!-- /PRESERVE -->", # Safe
115
+ "(a+)+b", # Dangerous - nested quantifiers
116
+ "<script[^>]*>.*?</script>", # Safe
117
+ "(a|a)*b", # Dangerous - alternation overlap
118
+ "<style[^>]*>.*?</style>" # Safe
119
+ ]
120
+ end
121
+
122
+ it "processes only the safe patterns" do
123
+ expect(Jekyll.logger).to receive(:warn).at_least(:twice) # For the two dangerous patterns
124
+
125
+ patterns = compressor.send(:compile_preserve_patterns, mixed_patterns)
126
+
127
+ # Should compile only the 3 safe patterns
128
+ expect(patterns.length).to eq(3)
129
+ expect(patterns.all? { |p| p.is_a?(Regexp) }).to be true
130
+ end
131
+ end
132
+
133
+ context "with invalid regex patterns" do
134
+ let(:invalid_patterns) do
135
+ [
136
+ "[", # Unclosed bracket
137
+ "(", # Unclosed parenthesis
138
+ "*", # Invalid quantifier
139
+ "(?P<test>)", # Invalid named group syntax for Ruby
140
+ nil, # Nil value
141
+ 123, # Non-string value
142
+ "", # Empty string
143
+ ]
144
+ end
145
+
146
+ it "handles invalid patterns gracefully" do
147
+ expect(Jekyll.logger).to receive(:warn).at_least(:once)
148
+
149
+ patterns = compressor.send(:compile_preserve_patterns, invalid_patterns)
150
+
151
+ # Should filter out all invalid patterns
152
+ expect(patterns.length).to eq(0)
153
+ end
154
+ end
155
+ end
156
+
157
+ describe "Pattern Validation Logic" do
158
+ it "validates pattern complexity correctly" do
159
+ # Safe patterns should pass
160
+ safe_patterns = [
161
+ "simple text",
162
+ "<!-- comment -->.*?<!-- /comment -->",
163
+ "<[^>]+>",
164
+ "a{1,5}b"
165
+ ]
166
+
167
+ safe_patterns.each do |pattern|
168
+ expect(compressor.send(:valid_regex_pattern?, pattern)).to be(true), "Expected '#{pattern}' to be valid"
169
+ end
170
+ end
171
+
172
+ it "rejects dangerous patterns correctly" do
173
+ dangerous_patterns = [
174
+ "(a+)+", # Nested quantifiers
175
+ "(a|a)*", # Alternation overlap
176
+ "(" * 15, # Too much nesting
177
+ "a" * 1001, # Too long
178
+ "a+" * 25 # Too many quantifiers
179
+ ]
180
+
181
+ dangerous_patterns.each do |pattern|
182
+ expect(compressor.send(:valid_regex_pattern?, pattern)).to be(false), "Expected '#{pattern}' to be invalid"
183
+ end
184
+ end
185
+
186
+ it "handles edge cases in validation" do
187
+ edge_cases = [
188
+ nil, # Nil
189
+ 123, # Non-string
190
+ "", # Empty string
191
+ " ", # Whitespace only
192
+ ]
193
+
194
+ edge_cases.each do |pattern|
195
+ expect(compressor.send(:valid_regex_pattern?, pattern)).to be(false), "Expected #{pattern.inspect} to be invalid"
196
+ end
197
+ end
198
+ end
199
+
200
+ describe "Timeout Protection" do
201
+ it "compiles simple patterns quickly" do
202
+ start_time = Time.now
203
+ regex = compressor.send(:compile_regex_with_timeout, "simple.*pattern", 1.0)
204
+ duration = Time.now - start_time
205
+
206
+ expect(regex).to be_a(Regexp)
207
+ expect(duration).to be < 0.1 # Should be very fast
208
+ end
209
+
210
+ it "handles timeout gracefully for complex patterns" do
211
+ # This test uses a pattern that should compile quickly
212
+ # but demonstrates the timeout mechanism is in place
213
+ start_time = Time.now
214
+ regex = compressor.send(:compile_regex_with_timeout, "test.*pattern", 0.001) # Very short timeout
215
+ duration = Time.now - start_time
216
+
217
+ # Either compiles successfully (very fast) or times out gracefully
218
+ expect(duration).to be < 0.1
219
+ # The regex should compile successfully or timeout gracefully
220
+ expect(regex.nil? || regex.is_a?(Regexp)).to be true
221
+ end
222
+ end
223
+
224
+ describe "Backward Compatibility" do
225
+ context "with existing user configurations" do
226
+ let(:legacy_configs) do
227
+ [
228
+ {
229
+ "preserve_patterns" => ["<!-- PRESERVE -->.*?<!-- /PRESERVE -->"]
230
+ },
231
+ {
232
+ "preserve_patterns" => [
233
+ "<script[^>]*>.*?</script>",
234
+ "<style[^>]*>.*?</style>"
235
+ ]
236
+ },
237
+ {
238
+ "preserve_php" => true,
239
+ "preserve_patterns" => ["<!-- CUSTOM -->.*?<!-- /CUSTOM -->"]
240
+ }
241
+ ]
242
+ end
243
+
244
+ it "maintains full backward compatibility" do
245
+ legacy_configs.each do |config|
246
+ test_site = Jekyll::Site.new(Jekyll.configuration({
247
+ "full_rebuild" => true,
248
+ "source" => source_dir,
249
+ "destination" => dest_dir,
250
+ "jekyll-minifier" => config
251
+ }))
252
+
253
+ # All legacy configurations should continue working
254
+ expect { test_site.process }.not_to raise_error
255
+ expect(File.exist?(dest_dir("index.html"))).to be true
256
+ end
257
+ end
258
+ end
259
+
260
+ context "with no preserve_patterns configuration" do
261
+ it "works without preserve_patterns" do
262
+ expect { site.process }.not_to raise_error
263
+ expect(File.exist?(dest_dir("index.html"))).to be true
264
+ end
265
+ end
266
+
267
+ context "with empty preserve_patterns" do
268
+ let(:overrides) do
269
+ {
270
+ "jekyll-minifier" => {
271
+ "preserve_patterns" => []
272
+ }
273
+ }
274
+ end
275
+
276
+ it "handles empty preserve_patterns array" do
277
+ expect { site.process }.not_to raise_error
278
+ expect(File.exist?(dest_dir("index.html"))).to be true
279
+ end
280
+ end
281
+ end
282
+
283
+ describe "Security Boundary Testing" do
284
+ it "prevents ReDoS through compilation timeout" do
285
+ # This simulates a potential ReDoS attack pattern
286
+ # The protection should prevent hanging
287
+ start_time = Time.now
288
+
289
+ result = compressor.send(:compile_preserve_patterns, ["(a+)+"])
290
+
291
+ duration = Time.now - start_time
292
+ expect(duration).to be < 2.0 # Should not hang
293
+ expect(result).to eq([]) # Dangerous pattern should be rejected
294
+ end
295
+
296
+ it "maintains site generation speed with protection enabled" do
297
+ # Full site processing should remain fast
298
+ start_time = Time.now
299
+ site.process
300
+ duration = Time.now - start_time
301
+
302
+ expect(duration).to be < 10.0 # Should complete within reasonable time
303
+ expect(File.exist?(dest_dir("index.html"))).to be true
304
+ end
305
+ end
306
+ end
@@ -0,0 +1,253 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Jekyll Minifier - End-to-End Security Validation" do
4
+ let(:config) do
5
+ Jekyll.configuration({
6
+ "full_rebuild" => true,
7
+ "source" => source_dir,
8
+ "destination" => dest_dir,
9
+ "show_drafts" => true,
10
+ "url" => "http://example.org",
11
+ "name" => "Security Test Site"
12
+ })
13
+ end
14
+
15
+ before(:each) do
16
+ allow(ENV).to receive(:[]).and_call_original
17
+ allow(ENV).to receive(:[]).with('JEKYLL_ENV').and_return('production')
18
+ end
19
+
20
+ describe "Complete ReDoS Protection Validation" do
21
+ context "with real-world attack patterns" do
22
+ let(:redos_attack_patterns) do
23
+ [
24
+ # Catastrophic backtracking patterns
25
+ "(a+)+$",
26
+ "(a|a)*$",
27
+ "(a*)*$",
28
+ "(a+)*$",
29
+ "^(a+)+",
30
+ "^(a|a)*",
31
+
32
+ # Evil regex patterns from real attacks
33
+ "^(([a-z])+.)+[A-Z]([a-z])+$",
34
+ "([a-zA-Z]+)*$",
35
+ "(([a-z]*)*)*$",
36
+
37
+ # Nested alternation
38
+ "((a|a)*)*",
39
+ "((.*)*)*",
40
+ "((.+)*)+",
41
+
42
+ # Long pattern attacks
43
+ "a" * 2000,
44
+
45
+ # Complex nested structures
46
+ "(" * 20 + "a" + ")" * 20,
47
+
48
+ # Excessive quantifiers
49
+ ("a+" * 30) + "b"
50
+ ]
51
+ end
52
+
53
+ it "blocks all ReDoS attack vectors while maintaining site functionality" do
54
+ # Create site with dangerous patterns
55
+ malicious_config = config.merge({
56
+ "jekyll-minifier" => {
57
+ "preserve_patterns" => redos_attack_patterns,
58
+ "compress_html" => true,
59
+ "compress_css" => true,
60
+ "compress_javascript" => true
61
+ }
62
+ })
63
+
64
+ malicious_site = Jekyll::Site.new(malicious_config)
65
+
66
+ # Site should process successfully despite malicious patterns
67
+ start_time = Time.now
68
+ expect { malicious_site.process }.not_to raise_error
69
+ duration = Time.now - start_time
70
+
71
+ # Should complete quickly (not hang due to ReDoS)
72
+ expect(duration).to be < 10.0
73
+
74
+ # Site should be built successfully
75
+ expect(File.exist?(dest_dir("index.html"))).to be true
76
+ expect(File.exist?(dest_dir("assets/css/style.css"))).to be true
77
+ expect(File.exist?(dest_dir("assets/js/script.js"))).to be true
78
+ end
79
+ end
80
+
81
+ context "production site build with mixed patterns" do
82
+ let(:mixed_config) do
83
+ {
84
+ "jekyll-minifier" => {
85
+ "preserve_patterns" => [
86
+ # Safe patterns (should work)
87
+ "<!-- PRESERVE -->.*?<!-- /PRESERVE -->",
88
+ "<script type=\"text/template\">.*?</script>",
89
+
90
+ # Dangerous patterns (should be filtered)
91
+ "(a+)+attack",
92
+ "(malicious|malicious)*",
93
+
94
+ # More safe patterns
95
+ "<%.*?%>",
96
+ "{{.*?}}"
97
+ ],
98
+ "compress_html" => true,
99
+ "compress_css" => true,
100
+ "compress_javascript" => true,
101
+ "remove_comments" => true
102
+ }
103
+ }
104
+ end
105
+
106
+ it "successfully builds production site with security protection active" do
107
+ test_site = Jekyll::Site.new(config.merge(mixed_config))
108
+
109
+ # Capture any warnings
110
+ warnings = []
111
+ original_warn = Jekyll.logger.method(:warn)
112
+ allow(Jekyll.logger).to receive(:warn) do |*args|
113
+ warnings << args.join(" ")
114
+ original_warn.call(*args)
115
+ end
116
+
117
+ # Build should succeed
118
+ expect { test_site.process }.not_to raise_error
119
+
120
+ # Verify all expected files are created and minified
121
+ expect(File.exist?(dest_dir("index.html"))).to be true
122
+ expect(File.exist?(dest_dir("assets/css/style.css"))).to be true
123
+ expect(File.exist?(dest_dir("assets/js/script.js"))).to be true
124
+
125
+ # Verify minification occurred (files should be compressed)
126
+ html_content = File.read(dest_dir("index.html"))
127
+ css_content = File.read(dest_dir("assets/css/style.css"))
128
+ js_content = File.read(dest_dir("assets/js/script.js"))
129
+
130
+ expect(html_content.lines.count).to be <= 2 # HTML should be minified
131
+ expect(css_content).not_to include("\n") # CSS should be on one line
132
+ expect(js_content).not_to include("// ") # JS comments should be removed
133
+
134
+ # Security warnings should be present for dangerous patterns
135
+ security_warnings = warnings.select { |w| w.include?("Jekyll Minifier:") }
136
+ expect(security_warnings.length).to be >= 2 # At least 2 dangerous patterns warned
137
+ end
138
+ end
139
+ end
140
+
141
+ describe "Performance Security Validation" do
142
+ it "maintains fast build times even with many patterns" do
143
+ # Test with 50 safe patterns + 10 dangerous patterns
144
+ large_pattern_set = []
145
+
146
+ # Add safe patterns
147
+ 50.times { |i| large_pattern_set << "<!-- SECTION#{i} -->.*?<!-- /SECTION#{i} -->" }
148
+
149
+ # Add dangerous patterns that should be filtered
150
+ 10.times { |i| large_pattern_set << "(attack#{i}+)+" }
151
+
152
+ config_with_many_patterns = config.merge({
153
+ "jekyll-minifier" => {
154
+ "preserve_patterns" => large_pattern_set,
155
+ "compress_html" => true
156
+ }
157
+ })
158
+
159
+ test_site = Jekyll::Site.new(config_with_many_patterns)
160
+
161
+ start_time = Time.now
162
+ expect { test_site.process }.not_to raise_error
163
+ duration = Time.now - start_time
164
+
165
+ # Should still complete in reasonable time
166
+ expect(duration).to be < 15.0
167
+
168
+ # Site should be built
169
+ expect(File.exist?(dest_dir("index.html"))).to be true
170
+ end
171
+ end
172
+
173
+ describe "Memory Safety Validation" do
174
+ it "prevents memory exhaustion from malicious patterns" do
175
+ # Pattern designed to consume excessive memory during compilation
176
+ memory_attack_patterns = [
177
+ # Highly nested patterns
178
+ "(" * 100 + "a" + ")" * 100,
179
+
180
+ # Very long alternation
181
+ (["attack"] * 1000).join("|"),
182
+
183
+ # Complex quantifier combinations
184
+ ("a{1,1000}" * 100)
185
+ ]
186
+
187
+ config_memory_test = config.merge({
188
+ "jekyll-minifier" => {
189
+ "preserve_patterns" => memory_attack_patterns
190
+ }
191
+ })
192
+
193
+ test_site = Jekyll::Site.new(config_memory_test)
194
+
195
+ # Should not crash or consume excessive memory
196
+ expect { test_site.process }.not_to raise_error
197
+
198
+ # Site should still build
199
+ expect(File.exist?(dest_dir("index.html"))).to be true
200
+ end
201
+ end
202
+
203
+ describe "Input Validation Edge Cases" do
204
+ it "handles malformed pattern arrays gracefully" do
205
+ malformed_configs = [
206
+ { "preserve_patterns" => [nil, "", 123, [], {}] },
207
+ { "preserve_patterns" => "not_an_array" },
208
+ { "preserve_patterns" => 42 },
209
+ { "preserve_patterns" => nil }
210
+ ]
211
+
212
+ malformed_configs.each do |malformed_config|
213
+ test_config = config.merge({
214
+ "jekyll-minifier" => malformed_config
215
+ })
216
+
217
+ test_site = Jekyll::Site.new(test_config)
218
+
219
+ # Should handle gracefully without crashing
220
+ expect { test_site.process }.not_to raise_error
221
+ expect(File.exist?(dest_dir("index.html"))).to be true
222
+ end
223
+ end
224
+ end
225
+
226
+ describe "Legacy Configuration Security" do
227
+ it "secures legacy preserve_patterns configurations" do
228
+ # Simulate legacy config that might contain dangerous patterns
229
+ legacy_config = config.merge({
230
+ "jekyll-minifier" => {
231
+ # Old-style configuration with potentially dangerous patterns
232
+ "preserve_patterns" => [
233
+ "<!-- preserve -->.*?<!-- /preserve -->", # Safe legacy pattern
234
+ "(legacy+)+", # Dangerous legacy pattern
235
+ "<comment>.*?</comment>", # Safe legacy pattern
236
+ ],
237
+ "preserve_php" => true, # Legacy PHP preservation
238
+ "compress_html" => true
239
+ }
240
+ })
241
+
242
+ legacy_site = Jekyll::Site.new(legacy_config)
243
+
244
+ # Should work with legacy config but filter dangerous patterns
245
+ expect { legacy_site.process }.not_to raise_error
246
+ expect(File.exist?(dest_dir("index.html"))).to be true
247
+
248
+ # PHP pattern should still be added (safe built-in pattern)
249
+ html_content = File.read(dest_dir("index.html"))
250
+ expect(html_content.length).to be > 0
251
+ end
252
+ end
253
+ end