aws-sdk-rails 5.1.0 → 5.2.0
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/CHANGELOG.md +7 -0
- data/VERSION +1 -1
- data/lib/aws/rails/middleware/elastic_beanstalk_sqsd/configuration.rb +28 -0
- data/lib/aws/rails/middleware/elastic_beanstalk_sqsd.rb +130 -18
- data/lib/aws-sdk-rails.rb +1 -0
- metadata +4 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 877222ef0a2075585720421300bd93c84591249678bafb02edf1e01962e63051
|
|
4
|
+
data.tar.gz: caed28f5f0bb7f604b5b2aeee08e59d5b8e6ad3612b4f9e8189b0a22e448ceeb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fc56e477c15a69e1773f305806118ba2aa7357840f68edbe6b491c569cdcb98ff417ad45abe38f620e5bc6b3c0010e00a1b0bedf987006f8ead442ab2dea4597
|
|
7
|
+
data.tar.gz: 33ede8ffc209b921330121a0f7642d1af4a97ce299dcc7613899f0b0322ec0e4e99b1ed098e601c08402496666a890d030502f9b41fe540c199d10f7bf6fa325
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
5.2.0 (2026-08-11)
|
|
2
|
+
------------------
|
|
3
|
+
|
|
4
|
+
* Issue - The `ElasticBeanstalkSQSD` middleware's Docker host check now only consults `remote_addr`, the raw TCP peer address, instead of also accepting `remote_ip`. `remote_ip` is derived from the client-supplied `X-Forwarded-For` header whenever the peer address is itself private, so a request from any private address could name the Docker gateway and be treated as local. Loopback peer addresses are accepted by the check, so requests proxied over loopback are unaffected.
|
|
5
|
+
* Issue - Only classes inheriting from `ActiveJob::Base` are executed by the `ElasticBeanstalkSQSD` middleware, preventing arbitrary classes named in an SQS message from being instantiated and run. Job class and periodic task names are validated as constant paths and resolved without searching the namespace's ancestors, so a request cannot name a constant outside the intended namespace.
|
|
6
|
+
* Feature - Adds a new configuration object for elastic beanstalk sqsd middleware, with an optional `job_class_allowlist` configuration to `ElasticBeanstalkSQSD` middleware to restrict which job classes can be dispatched. When set, the allowlist is checked before the class name is resolved, so an excluded class is never loaded.
|
|
7
|
+
|
|
1
8
|
5.1.0 (2024-12-05)
|
|
2
9
|
------------------
|
|
3
10
|
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
5.
|
|
1
|
+
5.2.0
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aws
|
|
4
|
+
module Rails
|
|
5
|
+
module Middleware
|
|
6
|
+
class ElasticBeanstalkSQSD
|
|
7
|
+
# Configuration for the {ElasticBeanstalkSQSD} middleware.
|
|
8
|
+
#
|
|
9
|
+
# Use {ElasticBeanstalkSQSD.config} to access the singleton config
|
|
10
|
+
# instance and {ElasticBeanstalkSQSD.configure} to configure in code:
|
|
11
|
+
#
|
|
12
|
+
# Aws::Rails::Middleware::ElasticBeanstalkSQSD.configure do |config|
|
|
13
|
+
# config.job_class_allowlist = [SendReceiptJob, ProcessOrderJob]
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
class Configuration
|
|
17
|
+
# @return [Array<Class, String>, nil] Optional list of job classes
|
|
18
|
+
# permitted to be executed. When set, only classes in this list
|
|
19
|
+
# will be dispatched. When nil, any class inheriting from
|
|
20
|
+
# ActiveJob::Base is allowed. Entries may be given as Class objects
|
|
21
|
+
# or Strings; matching is by class name, so an allowlisted class is
|
|
22
|
+
# still recognized after a Zeitwerk reload replaces the class object.
|
|
23
|
+
attr_accessor :job_class_allowlist
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -4,7 +4,42 @@ module Aws
|
|
|
4
4
|
module Rails
|
|
5
5
|
module Middleware
|
|
6
6
|
# Middleware to handle requests from the SQS Daemon present on Elastic Beanstalk worker environments.
|
|
7
|
-
|
|
7
|
+
#
|
|
8
|
+
# See {Configuration} for the available options and {configure} for
|
|
9
|
+
# setting them in code.
|
|
10
|
+
class ElasticBeanstalkSQSD # rubocop:disable Metrics/ClassLength
|
|
11
|
+
# Raised when a job message names a class that cannot be executed -
|
|
12
|
+
# the name is not a well-formed constant path, it is not in the
|
|
13
|
+
# configured job_class_allowlist, or it does not inherit from
|
|
14
|
+
# ActiveJob::Base.
|
|
15
|
+
class InvalidJobClassError < StandardError; end
|
|
16
|
+
|
|
17
|
+
# A job class name is an (optionally namespaced) constant path and
|
|
18
|
+
# nothing else. Names that cannot match this can never name a job
|
|
19
|
+
# class, so rejecting them keeps them out of the constant lookup.
|
|
20
|
+
JOB_CLASS_NAME_PATTERN = /\A[A-Z]\w*(::[A-Z]\w*)*\z/.freeze
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
# Yields the middleware configuration for customization.
|
|
24
|
+
#
|
|
25
|
+
# @example Restrict job dispatch to specific classes
|
|
26
|
+
# Aws::Rails::Middleware::ElasticBeanstalkSQSD.configure do |config|
|
|
27
|
+
# config.job_class_allowlist = [SendReceiptJob, ProcessOrderJob]
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# @yieldparam [Configuration] config
|
|
31
|
+
# @return [Configuration]
|
|
32
|
+
def configure
|
|
33
|
+
yield(config) if block_given?
|
|
34
|
+
config
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [Configuration] the current middleware configuration.
|
|
38
|
+
def config
|
|
39
|
+
@config ||= Configuration.new
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
8
43
|
def initialize(app)
|
|
9
44
|
@app = app
|
|
10
45
|
@logger = ::Rails.logger
|
|
@@ -22,7 +57,9 @@ module Aws
|
|
|
22
57
|
|
|
23
58
|
@logger.debug('aws-sdk-rails middleware detected call from Elastic Beanstalk SQS Daemon.')
|
|
24
59
|
|
|
25
|
-
#
|
|
60
|
+
# Best-effort source check. On Docker-based EB platforms this is unreliable
|
|
61
|
+
# because all traffic arrives via the docker bridge. Use job_class_allowlist
|
|
62
|
+
# and network-level controls (security groups) as the primary safeguards.
|
|
26
63
|
unless request.local? || sent_from_docker_host?(request)
|
|
27
64
|
@logger.warn('SQSD request detected from untrusted address; returning 403 forbidden.')
|
|
28
65
|
return forbidden_response
|
|
@@ -78,43 +115,55 @@ module Aws
|
|
|
78
115
|
# Jobs queued from the SQS adapter contain the JSON message in the request body.
|
|
79
116
|
job = ::ActiveSupport::JSON.decode(request.body.string)
|
|
80
117
|
job_name = job['job_class']
|
|
118
|
+
# Scope the rescue to resolution only. A NameError raised from inside
|
|
119
|
+
# the job's own #perform must not be mislabeled as a class-resolution
|
|
120
|
+
# failure, so ::ActiveJob::Base.execute runs outside the begin/rescue.
|
|
121
|
+
begin
|
|
122
|
+
resolve_job_class(job_name)
|
|
123
|
+
rescue NameError, InvalidJobClassError => e
|
|
124
|
+
return unresolved_job_class_response(job_name, e)
|
|
125
|
+
end
|
|
81
126
|
@logger.debug("Executing job: #{job_name}")
|
|
82
127
|
::ActiveJob::Base.execute(job)
|
|
83
128
|
[200, { 'Content-Type' => 'text/plain' }, ["Successfully ran job #{job_name}."]]
|
|
84
|
-
rescue NameError => e
|
|
85
|
-
@logger.error("Job #{job_name} could not resolve to a class that inherits from Active Job.")
|
|
86
|
-
@logger.error("Error: #{e}")
|
|
87
|
-
internal_error_response
|
|
88
129
|
end
|
|
89
130
|
|
|
90
131
|
# Execute a job using the thread pool executor
|
|
91
132
|
def _execute_job_background(request)
|
|
92
133
|
job_data = ::ActiveSupport::JSON.decode(request.body.string)
|
|
93
|
-
|
|
134
|
+
job_name = job_data['job_class']
|
|
135
|
+
resolve_job_class(job_name)
|
|
136
|
+
@logger.debug("Queuing background job: #{job_name}")
|
|
94
137
|
@executor.post(job_data) do |job|
|
|
95
138
|
::ActiveJob::Base.execute(job)
|
|
96
139
|
end
|
|
97
|
-
[200, { 'Content-Type' => 'text/plain' }, ["Successfully queued job #{
|
|
140
|
+
[200, { 'Content-Type' => 'text/plain' }, ["Successfully queued job #{job_name}"]]
|
|
98
141
|
rescue Concurrent::RejectedExecutionError
|
|
99
142
|
msg = 'No capacity to execute job.'
|
|
100
143
|
@logger.info(msg)
|
|
101
144
|
[429, { 'Content-Type' => 'text/plain' }, [msg]]
|
|
145
|
+
rescue NameError, InvalidJobClassError => e
|
|
146
|
+
unresolved_job_class_response(job_name, e)
|
|
102
147
|
end
|
|
103
148
|
|
|
104
149
|
def execute_periodic_task(request)
|
|
105
150
|
# The beanstalk worker SQS Daemon will add the 'X-Aws-Sqsd-Taskname' for periodic tasks set in cron.yaml.
|
|
106
151
|
job_name = request.headers['X-Aws-Sqsd-Taskname']
|
|
107
|
-
|
|
152
|
+
# Resolve once and reuse the class, rather than resolving the name a
|
|
153
|
+
# second time with another constantize (which also left a check-then-
|
|
154
|
+
# act gap between what was validated and what ran). Scope the rescue
|
|
155
|
+
# to resolution only, so a NameError from inside the task's own
|
|
156
|
+
# #perform is not mislabeled as a cron-name resolution failure.
|
|
157
|
+
begin
|
|
158
|
+
job = resolve_job_class(job_name).new
|
|
159
|
+
rescue NameError, InvalidJobClassError => e
|
|
160
|
+
return unresolved_periodic_task_response(job_name, e)
|
|
161
|
+
end
|
|
108
162
|
if @executor
|
|
109
163
|
_execute_periodic_task_background(job)
|
|
110
164
|
else
|
|
111
165
|
_execute_periodic_task_now(job)
|
|
112
166
|
end
|
|
113
|
-
rescue NameError => e
|
|
114
|
-
@logger.error("Periodic task #{job_name} could not resolve to an Active Job class " \
|
|
115
|
-
'- check the cron name spelling and set the path as / in cron.yaml.')
|
|
116
|
-
@logger.error("Error: #{e}.")
|
|
117
|
-
internal_error_response
|
|
118
167
|
end
|
|
119
168
|
|
|
120
169
|
def _execute_periodic_task_now(job)
|
|
@@ -138,6 +187,19 @@ module Aws
|
|
|
138
187
|
[500, { 'Content-Type' => 'text/plain' }, [message]]
|
|
139
188
|
end
|
|
140
189
|
|
|
190
|
+
def unresolved_job_class_response(job_name, error)
|
|
191
|
+
@logger.error("Job #{job_name} could not resolve to a class that inherits from Active Job.")
|
|
192
|
+
@logger.error("Error: #{error}")
|
|
193
|
+
internal_error_response
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def unresolved_periodic_task_response(job_name, error)
|
|
197
|
+
@logger.error("Periodic task #{job_name} could not resolve to an Active Job class " \
|
|
198
|
+
'- check the cron name spelling and set the path as / in cron.yaml.')
|
|
199
|
+
@logger.error("Error: #{error}.")
|
|
200
|
+
internal_error_response
|
|
201
|
+
end
|
|
202
|
+
|
|
141
203
|
def forbidden_response
|
|
142
204
|
message = 'Request with aws-sqsd user agent was made from untrusted address.'
|
|
143
205
|
[403, { 'Content-Type' => 'text/plain' }, [message]]
|
|
@@ -156,6 +218,48 @@ module Aws
|
|
|
156
218
|
request.headers['X-Aws-Sqsd-Taskname'].present? && request.fullpath == '/'
|
|
157
219
|
end
|
|
158
220
|
|
|
221
|
+
# Resolves +name+ to a job class, raising InvalidJobClassError if it is
|
|
222
|
+
# not a well-formed constant path, is not in the configured allowlist,
|
|
223
|
+
# or does not name a class inheriting from ActiveJob::Base. Returns the
|
|
224
|
+
# resolved class so callers can reuse it without resolving the name a
|
|
225
|
+
# second time.
|
|
226
|
+
#
|
|
227
|
+
# Checks are ordered so that the least trusting one runs first. Merely
|
|
228
|
+
# resolving a constant is not inert: under Zeitwerk it triggers
|
|
229
|
+
# autoloading, which runs the body of whichever file defines it. So the
|
|
230
|
+
# name is matched against the allowlist as a String, before any lookup,
|
|
231
|
+
# and an excluded name never reaches the constant resolver at all.
|
|
232
|
+
def resolve_job_class(name)
|
|
233
|
+
name = name.to_s
|
|
234
|
+
raise InvalidJobClassError, "#{name.inspect} is not a valid job class name" unless
|
|
235
|
+
JOB_CLASS_NAME_PATTERN.match?(name)
|
|
236
|
+
|
|
237
|
+
allowlist = self.class.config.job_class_allowlist
|
|
238
|
+
# Match by class name, not object identity: in development Zeitwerk
|
|
239
|
+
# reloads produce a new class object with the same name, so an
|
|
240
|
+
# allowlist of Class objects would reject every job after a reload.
|
|
241
|
+
if allowlist && !allowlist.map(&:to_s).include?(name)
|
|
242
|
+
raise InvalidJobClassError, "#{name} is not in the configured job_class_allowlist"
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
klass = constantize_job_class(name)
|
|
246
|
+
unless klass.is_a?(Class) && klass < ::ActiveJob::Base
|
|
247
|
+
raise InvalidJobClassError, "#{name} is not a valid job class (must inherit from ActiveJob::Base)"
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
klass
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Resolves an already-validated name. +inherit+ is false so that each
|
|
254
|
+
# segment must be defined directly on the preceding one: a name such as
|
|
255
|
+
# 'SomeJob::Foo' cannot resolve Foo from SomeJob's ancestors when
|
|
256
|
+
# SomeJob itself does not define it.
|
|
257
|
+
def constantize_job_class(name)
|
|
258
|
+
# CodeQL [rb/code-injection] Name is validated against JOB_CLASS_NAME_PATTERN
|
|
259
|
+
# and the allowlist above, and the result is checked for ActiveJob::Base ancestry below.
|
|
260
|
+
Object.const_get(name, false)
|
|
261
|
+
end
|
|
262
|
+
|
|
159
263
|
def sent_from_docker_host?(request)
|
|
160
264
|
app_runs_in_docker_container? && ip_originates_from_docker_host?(request)
|
|
161
265
|
end
|
|
@@ -172,16 +276,24 @@ module Aws
|
|
|
172
276
|
File.exist?('/proc/self/mountinfo') && File.read('/proc/self/mountinfo') =~ %r{/docker/containers/}
|
|
173
277
|
end
|
|
174
278
|
|
|
279
|
+
# Only consults remote_addr, the raw TCP peer address. remote_ip is
|
|
280
|
+
# derived from X-Forwarded-For whenever the peer is itself private, and
|
|
281
|
+
# the docker gateway range (172.16.0.0/12) is one Rack treats as a
|
|
282
|
+
# trusted proxy to see through - so a request from any private peer can
|
|
283
|
+
# set remote_ip to the gateway with a header alone. Loopback is accepted
|
|
284
|
+
# here because such a request is genuinely local; request.local? rejects
|
|
285
|
+
# it only on the forgeable half of its check.
|
|
175
286
|
def ip_originates_from_docker_host?(request)
|
|
176
|
-
|
|
177
|
-
|
|
287
|
+
remote_addr = request.remote_addr
|
|
288
|
+
default_docker_ips.include?(remote_addr) ||
|
|
289
|
+
::ActionDispatch::Request::LOCALHOST.match?(remote_addr)
|
|
178
290
|
end
|
|
179
291
|
|
|
180
292
|
def default_docker_ips
|
|
181
293
|
@default_docker_ips ||= build_default_docker_ips
|
|
182
294
|
end
|
|
183
295
|
|
|
184
|
-
# rubocop:disable Metrics/AbcSize
|
|
296
|
+
# rubocop:disable Metrics/AbcSize, Style/FileOpen
|
|
185
297
|
def build_default_docker_ips
|
|
186
298
|
default_gw_ips = ['172.17.0.1']
|
|
187
299
|
|
|
@@ -198,7 +310,7 @@ module Aws
|
|
|
198
310
|
|
|
199
311
|
default_gw_ips
|
|
200
312
|
end
|
|
201
|
-
# rubocop:enable Metrics/AbcSize
|
|
313
|
+
# rubocop:enable Metrics/AbcSize, Style/FileOpen
|
|
202
314
|
end
|
|
203
315
|
end
|
|
204
316
|
end
|
data/lib/aws-sdk-rails.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: aws-sdk-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.
|
|
4
|
+
version: 5.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Amazon Web Services
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: aws-sdk-core
|
|
@@ -50,13 +49,13 @@ files:
|
|
|
50
49
|
- VERSION
|
|
51
50
|
- lib/aws-sdk-rails.rb
|
|
52
51
|
- lib/aws/rails/middleware/elastic_beanstalk_sqsd.rb
|
|
52
|
+
- lib/aws/rails/middleware/elastic_beanstalk_sqsd/configuration.rb
|
|
53
53
|
- lib/aws/rails/notifications.rb
|
|
54
54
|
- lib/aws/rails/railtie.rb
|
|
55
55
|
homepage: https://github.com/aws/aws-sdk-rails
|
|
56
56
|
licenses:
|
|
57
57
|
- Apache-2.0
|
|
58
58
|
metadata: {}
|
|
59
|
-
post_install_message:
|
|
60
59
|
rdoc_options: []
|
|
61
60
|
require_paths:
|
|
62
61
|
- lib
|
|
@@ -71,8 +70,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
71
70
|
- !ruby/object:Gem::Version
|
|
72
71
|
version: '0'
|
|
73
72
|
requirements: []
|
|
74
|
-
rubygems_version:
|
|
75
|
-
signing_key:
|
|
73
|
+
rubygems_version: 4.0.16
|
|
76
74
|
specification_version: 4
|
|
77
75
|
summary: AWS SDK for Ruby on Rails Railtie
|
|
78
76
|
test_files: []
|