Course outline

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.

About 5 min readContent reviewed
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.

ruby A closed action map for synthetic records
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)
Expected output
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.

ruby archive_format.rb: the fixed local helper
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.

ruby app.rb: an explicit local require
require_relative 'archive_format'
puts ArchiveFormat.label('A-8')
Expected output
[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.

Official references