Ruby / Integration: data, resources, runtime evidence, and trust / Lesson 17
Keep loading and dispatch separate from untrusted data
Use fixed load paths, closed action maps, explicit data parsing, and bounded inputs without turning user text into executable Ruby.
On this page
Lesson goals
- Distinguish code loading, data parsing, and resource authorization.
- Use an explicit allowlist rather than arbitrary method dispatch from external strings.
A flexible runtime needs explicit trust boundaries
Ruby can load files, evaluate code, dispatch methods dynamically, and restore serialized objects. These are powerful capabilities, not generic ways to parse user data. Keep untrusted input as data and translate it into a small validated internal model. A method being public does not authorize an external caller to choose it arbitrarily. Likewise, an identifier matching a regexp does not establish resource ownership. Define the actor, permitted action, resource, and current-state rule at the authoritative boundary. This lesson uses synthetic local inputs and performs no attack, scan, account operation, or host command.
Map external choices to a closed set of operations
The action map below accepts only count and ids, with an explicit unsupported result for other names. It does not feed the name to eval, send, or an unrestricted public_send. The handlers themselves still need valid records and resource authorization in a real application. Freezing the outer map prevents ordinary map mutation but does not turn every reachable object into an immutable or safe capability. Keep the set small and review each handler’s effects. The result shown is analytical only; no Ruby interpreter was available to execute it.
ACTIONS = {
'count' => ->(records) { records.length.to_s },
'ids' => ->(records) { records.map { |record| record.fetch(:id) }.join(',') }
}.freeze
def dispatch(name, records)
handler = ACTIONS[name]
return 'unsupported action' unless handler
handler.call(records)
end
records = [{id: 'A-1'}, {id: 'A-2'}]
puts dispatch('count', records)
puts dispatch('unknown', records)
2
unsupported action
Expected output is 2 then unsupported action. This constrains dispatch names only; it does not implement authentication or validate every record. The script was not run.
A require target is trusted code, not an input field
require loads a recognized feature under Ruby’s loading rules, while load can reevaluate source. require_relative resolves relative to the requiring file rather than a caller’s arbitrary working directory. Never derive a load target directly from an untrusted request. A Gemfile and gem code are executable dependencies whose provenance and compatibility belong to the project’s dependency process. The two-file example uses a fixed local helper name and is illustrative only: those files are not created or loaded on the authoring host. Choose namespacing and explicit imports to keep effects understandable.
Make the loaded helper and its API explicit
archive_format.rb defines one namespaced operation, and the entry file explicitly requires that local file before calling it. Both source files must exist together in a learner-owned isolated directory to execute the example. Neither block alone is the complete program. This is a loading-layout example, not a sandbox: executing trusted Ruby code can still perform whatever its implementation does. Review top-level effects in loaded files and avoid hidden service connections or global patches during import. A fixed require path improves predictability but does not certify the loaded code’s quality or safety.
module ArchiveFormat
def self.label(id)
"[#{id}]"
end
end
This file defines a module method and has no output by itself. It is one part of the two-file illustrative program and was not loaded.
require_relative 'archive_format'
puts ArchiveFormat.label('A-8')
[A-8]
When placed beside the preceding helper, expected output is [A-8]. The files were not created or executed during authoring.
Select data APIs that do not restore arbitrary behavior
Marshal.load is unsuitable for untrusted data, and unsafe object-loading modes in other serializers require the same careful boundary reasoning. Prefer a restricted data format and explicit validation; JSON parsing without object additions still needs shape, size, and domain checks. Avoid interpolating input into shell or regular-expression syntax as if escaping were universal across contexts. Keep error logs minimal and safe. In a real project, verify closed dispatch, rejected unknown fields, load-path assumptions, and serialization policy in an isolated authorized environment. This course executes no dynamic evaluation, deserialization attack, subprocess, dependency installation, or security test.
Common errors and fixes
A request chooses an unintended public method.
- Why it happens
- External text was passed into unrestricted dynamic dispatch.
- How to fix it
- Translate through a closed action map and authorize the resource operation separately.
- Prevent it next time
- Review available actions rather than relying on method visibility alone.
Loading a user-selected path executes unexpected code.
- Why it happens
- Code loading was treated as data retrieval.
- How to fix it
- Keep load targets fixed or strictly application-owned and use data readers for data.
- Prevent it next time
- Separate dependency provenance from input validation.
A serialized object is trusted merely because parsing succeeded.
- Why it happens
- Object restoration was confused with constrained data decoding.
- How to fix it
- Choose a safe data boundary and validate the resulting schema explicitly.
- Prevent it next time
- Review parser modes and allowed object construction before accepting external payloads.
Check your understanding
1. Does using public_send instead of send make arbitrary external method names safe?
Show worked solution
No. It limits ordinary visibility but can still expose public methods the product never intended the caller to choose. Use a closed mapping, validate arguments, and enforce actor/resource authorization. Treat dispatch restriction and permission as separate checks rather than assuming a language visibility rule is an application policy.
2. Why does a fixed require_relative path not make a helper a security sandbox?
Show worked solution
The loaded Ruby file is executable code with the capabilities of its process. A fixed path controls selection, not the implementation’s effects or trustworthiness. Review provenance, top-level behavior, and dependency policy. Use proper isolation for untrusted code instead of assuming namespacing or local loading limits what it can do.
Key takeaways
- Code loading, parsing, dispatch selection, and authorization are distinct boundaries.
- Flexible Ruby features need narrow application contracts, not hidden assumptions.