railspress-engine 1.4.3 → 1.4.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5a843f07c6d903605a67314573c816bbff6099640947e622e9c59a89ff88cbea
4
- data.tar.gz: 17f4e1697c1dd88947460d5e93cb42cee3652677ce69fc456e4da1821e56b9b1
3
+ metadata.gz: 48860d966cd3cdc8832ba51098ea94bf8b7264608105231af1b7871d4576d1c8
4
+ data.tar.gz: e4e9e1631fa9127c0c221e08c081e5280e85a7f7f7f3716814033f722aac921b
5
5
  SHA512:
6
- metadata.gz: 9bb75c97d3ace5d1fb54d6281d6ce4c2d17ac29cfd3d71821ba92cb22ecb7b4634a60b1015b0226755b52bc75e778d5ef9f1062976382843a03faa6b686990c8
7
- data.tar.gz: 2aaca7f4a20c3f875bac0c37817f5eadb853c317ef72bf9d00100ad5ddf865ed000214b8e0e15155dd90033b21a1b1426a7ce3f576d610e5819398e6aa535ca7
6
+ metadata.gz: 0523661e660f0d5490b95282ed786669282ea2edfe29a84397fafd10c22e1c599d383a9a40c4b3e2c1e9efa4476a82e24a5236a231c44d27041355986a100803
7
+ data.tar.gz: 699ae2eba634ef60e19f7c18b8f98bbd3470c61dcaa750eeff7d6ece5a07436bd33ab19e37740540f93f3621c3961bb3066abcb650717d1658cbc8b24232a97d
data/README.md CHANGED
@@ -157,6 +157,16 @@ end
157
157
 
158
158
  See [CONFIGURING.md](docs/CONFIGURING.md) for more authentication patterns including Devise integration.
159
159
 
160
+ ## Import Security
161
+
162
+ RailsPress stores imported files inside engine-owned temporary directories.
163
+
164
+ ZIP image references cannot escape their extraction directory.
165
+
166
+ Remote header images accept public HTTP and HTTPS destinations. Private and redirected destinations are rejected.
167
+
168
+ Configure request and worker limits for additional availability protection.
169
+
160
170
  ## Quick Start
161
171
 
162
172
  Access the admin at `/railspress/admin`. From there you can manage posts, entities, and blocks.
@@ -1,6 +1,8 @@
1
1
  module Railspress
2
2
  module Admin
3
3
  class ImportsController < BaseController
4
+ SUPPORTED_IMPORT_EXTENSIONS = %w[.md .markdown .txt .zip].freeze
5
+
4
6
  before_action :validate_import_type, only: [ :show ]
5
7
 
6
8
  def show
@@ -53,11 +55,18 @@ module Railspress
53
55
  FileUtils.mkdir_p(upload_dir)
54
56
 
55
57
  uploaded_files.map do |file|
56
- path = upload_dir.join(file.original_filename)
58
+ path = upload_dir.join(stored_upload_name(file))
57
59
  File.open(path, "wb") { |f| f.write(file.read) }
58
60
  path.to_s
59
61
  end
60
62
  end
63
+
64
+ def stored_upload_name(file)
65
+ extension = File.extname(file.original_filename.to_s).downcase
66
+ extension = "" unless SUPPORTED_IMPORT_EXTENSIONS.include?(extension)
67
+
68
+ "#{SecureRandom.hex(16)}#{extension}"
69
+ end
61
70
  end
62
71
  end
63
72
  end
@@ -30,12 +30,15 @@ module Railspress
30
30
  end
31
31
 
32
32
  def cleanup_uploaded_files(file_paths)
33
- tmp_dir = Rails.root.join("tmp").to_s
33
+ tmp_dir = File.realpath(Rails.root.join("tmp"))
34
34
 
35
35
  file_paths.each do |path|
36
- # Only cleanup files in the tmp directory to avoid deleting source files
37
- next unless path.start_with?(tmp_dir)
38
- FileUtils.rm_f(path) if File.exist?(path)
36
+ next unless File.exist?(path)
37
+
38
+ resolved_path = File.realpath(path)
39
+ next unless resolved_path.start_with?("#{tmp_dir}#{File::SEPARATOR}")
40
+
41
+ FileUtils.rm_f(resolved_path)
39
42
  end
40
43
  rescue => e
41
44
  Rails.logger.warn "Failed to cleanup uploaded import files: #{e.message}"
@@ -1,8 +1,9 @@
1
1
  require "yaml"
2
2
  require "zip"
3
- require "open-uri"
4
3
  require "fileutils"
5
4
  require "redcarpet"
5
+ require "ssrf_filter"
6
+ require "stringio"
6
7
 
7
8
  module Railspress
8
9
  class PostImportProcessor
@@ -211,29 +212,20 @@ module Railspress
211
212
  filename = File.basename(uri.path)
212
213
  filename = "header_image#{File.extname(uri.path)}" if filename.blank?
213
214
 
214
- downloaded = URI.open(url)
215
+ response = SsrfFilter.get(url, max_redirects: 0)
216
+ response.value
215
217
  post.header_image.attach(
216
- io: downloaded,
218
+ io: StringIO.new(response.body),
217
219
  filename: filename,
218
- content_type: downloaded.content_type
220
+ content_type: response.content_type
219
221
  )
220
- rescue OpenURI::HTTPError, URI::InvalidURIError => e
222
+ rescue SsrfFilter::Error, URI::InvalidURIError, Net::HTTPError => e
221
223
  Rails.logger.warn "Failed to download header image from #{url}: #{e.message}"
222
224
  end
223
225
 
224
226
  def attach_image_from_zip(post, relative_path, source_path, base_dir)
225
- # Resolve relative path from the markdown file's directory or from base_dir
226
- source_dir = File.dirname(source_path)
227
-
228
- # Try relative to the markdown file first
229
- image_path = File.expand_path(relative_path, source_dir)
230
-
231
- # If not found, try relative to base_dir
232
- unless File.exist?(image_path)
233
- image_path = File.expand_path(relative_path, base_dir)
234
- end
235
-
236
- return unless File.exist?(image_path)
227
+ image_path = imported_image_path(relative_path, source_path, base_dir)
228
+ return unless image_path
237
229
 
238
230
  ext = File.extname(image_path).downcase
239
231
  return unless IMAGE_EXTENSIONS.include?(ext)
@@ -253,6 +245,23 @@ module Railspress
253
245
  )
254
246
  end
255
247
 
248
+ def imported_image_path(relative_path, source_path, base_dir)
249
+ import_root = File.realpath(base_dir)
250
+ source_dir = File.dirname(source_path)
251
+
252
+ [ source_dir, import_root ].each do |directory|
253
+ candidate = File.expand_path(relative_path.to_s, directory)
254
+ next unless File.file?(candidate)
255
+
256
+ resolved_candidate = File.realpath(candidate)
257
+ return resolved_candidate if resolved_candidate.start_with?("#{import_root}#{File::SEPARATOR}")
258
+ end
259
+
260
+ nil
261
+ rescue Errno::EACCES, Errno::ENOENT
262
+ nil
263
+ end
264
+
256
265
  def extract_title_from_body(body)
257
266
  # Look for first H1: # Title
258
267
  if match = body.match(/^#\s+(.+)$/)
@@ -1,3 +1,3 @@
1
1
  module Railspress
2
- VERSION = "1.4.3"
2
+ VERSION = "1.4.4"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: railspress-engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.3
4
+ version: 1.4.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Avi Flombaum
@@ -85,6 +85,20 @@ dependencies:
85
85
  - - ">="
86
86
  - !ruby/object:Gem::Version
87
87
  version: '3.6'
88
+ - !ruby/object:Gem::Dependency
89
+ name: ssrf_filter
90
+ requirement: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - "~>"
93
+ - !ruby/object:Gem::Version
94
+ version: '1.5'
95
+ type: :runtime
96
+ prerelease: false
97
+ version_requirements: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - "~>"
100
+ - !ruby/object:Gem::Version
101
+ version: '1.5'
88
102
  - !ruby/object:Gem::Dependency
89
103
  name: rspec-rails
90
104
  requirement: !ruby/object:Gem::Requirement