skip to content

In Ruby, what can Formats.const_get(params[:format]).new instantiate when the name comes from the request, and how do you restrict it?

level: middleimportance: should knowfreq 34%

answer

  1. inherit defaults to true
  2. modules fall back to Object
  3. leading :: restarts at Object
  4. NameError: wrong constant name
  5. frozen Hash of names to classes

basics

~20 s

Any constant reachable from Formats: with the default inherit flag a module lookup also searches Object, so "File" returns ::File, and a leading "::" restarts at Object even with inherit false. Map allowed names to classes in a frozen hash instead.

solid answer

~40 s

`Module#const_get` takes a symbol or a string, and a string may be a path like `"Formats::Csv"`. With the default `inherit = true`, a lookup on a module also searches `Object`, so `Formats.const_get("File")` returns the core `File` class. Passing `false` stops that for plain names, but a leading `"::"` resets the lookup to `Object`, so `Formats.const_get("::File", false)` still returns `File`. Invalid names raise `NameError` ("wrong constant name") and missing ones raise `NameError` or trigger `const_missing` and autoloads, which can load code. Whatever comes back then receives `.new` with request-controlled arguments. The fix is an explicit registry: `FORMATS = { "csv" => Formats::Csv, "pdf" => Formats::Pdf }.freeze` and `FORMATS.fetch(name)`.

code

ruby · 10 lines
ruby
module Formats
  class Csv; end
  class Pdf; end
end

Formats.const_get("Csv")            # => Formats::Csv
Formats.const_get("File")           # => File (Object is searched too)
Formats.const_get("File", false)    # NameError: uninitialized constant Formats::File
Formats.const_get("::File", false)  # => File (leading :: restarts at Object)
Formats.const_get("csv")            # NameError: wrong constant name csv

go deeper

for a junior

Know that const_get turns a string into a class, so a string from a request can pick classes you never meant to expose.

for a middle

Explain the inherit flag, the Object fallback for modules, the leading :: reset and the NameError cases, then show a registry hash.

for a senior

Point out that the lookup itself can autoload files or hit const_missing, so type checks after it are defence in depth, not the boundary.

for a principal

Push reflection out of request paths entirely: registries declared per feature are reviewable, and reflection on names is not.

## How const_get resolves a name `Module#const_get(name, inherit = true)` looks up a constant by name at runtime and returns its value, raising `NameError` when nothing is found. It accepts a symbol or a string, and a string may contain a **namespace path** such as `"Formats::Csv::Writer"`, which is resolved segment by segment. Because classes are just constants holding `Class` objects, `const_get` is the usual way to turn a name into a class, which is why it shows up in "choose an exporter by format" code. Three rules decide what a request can reach: | Call | Result | Why | |---|---|---| | `Formats.const_get("Csv")` | `Formats::Csv` | found directly in `Formats` | | `Formats.const_get("File")` | `File` | with `inherit` true, a module lookup also searches `Object` | | `Formats.const_get("File", false)` | `NameError` | only `Formats` itself is searched | | `Formats.const_get("::File", false)` | `File` | a leading `::` restarts the lookup at `Object` | | `Formats.const_get("csv")` | `NameError` | "wrong constant name": constants start with a capital | So calling `const_get` on your own namespace **is not a boundary**. With the defaults, every top-level constant (`File`, `Dir`, `IO`, `Process`, `Kernel`, every gem's modules) is one request away, and the `inherit` flag does not survive a leading `::`. ## What happens after the lookup The returned object then receives whatever the code does next: - **`.new` with request arguments.** If the arguments also come from the request, `"File"` plus a path and a mode can create or truncate files. Classes that need a block or specific arguments simply raise, which is a crash rather than an exploit, but you cannot enumerate every class in the process. - **Non-class constants.** A name such as `"RUBY_VERSION"` returns a `String`, and calling `.new` on it raises `NoMethodError`, which leaks information through error pages. - **Side effects of the lookup itself.** A constant registered with `autoload`, or a module that overrides `const_missing` (as code loaders do), loads files when asked for a name. The lookup can therefore execute code before you inspect the result. One thing is **no longer** a risk: turning arbitrary strings into names used to create symbols that were never freed. Ruby's security guide notes that `const_get`, `to_sym` and `respond_to?` are no longer a denial-of-service vector on that front. ## Restricting it 1. **Use an explicit registry.** A frozen `Hash` from the strings clients may send to the classes you mean: `FORMATS.fetch(name) { raise ArgumentError, "unsupported format" }`. The request never names a constant at all. 2. **Or check a list before the lookup.** `ALLOWED = %w[Csv Pdf].freeze` and `raise unless ALLOWED.include?(name)`, then `Formats.const_get(name, false)`. This keeps the list and the classes in sync by name, at the cost of one more thing to keep aligned. 3. **Treat a type check after the lookup as a second layer, not the first.** `klass.is_a?(Class) && klass < Formats::Base` rejects foreign classes, but only after autoloads or `const_missing` already ran. 4. **Return a client error for unknown names**, not a 500 with a `NameError` message that echoes the attempted constant. ## Why interviewers ask The bug looks idiomatic: `const_get` is the documented way to go from a name to a class, and "it only searches my namespace" is a natural but false belief. A strong answer names the `Object` fallback, the `::` reset and the side effects of the lookup, then replaces reflection with a table. - **Weak answer:** "I call it on my module, so only my classes are reachable." - **Strong answer:** "The lookup reaches every top-level constant; I map names to classes explicitly and never reflect on request strings." ## Where the pattern hides - **Type fields in JSON bodies.** A payload such as `{"type": "PdfExport", ...}` that picks a class by name is the same sink in a different costume; `Object.const_get(body["type"])` is worse than the namespaced version, not better. - **Built names.** `Formats.const_get("#{kind.capitalize}Exporter")` still lets the request choose among every constant ending in that suffix that the lookup can reach, including ones added later for other purposes. - **Stored definitions.** A saved report whose format was chosen by a user is request data read back later; it deserves the same table. In each case the fix is identical: keep the vocabulary the client may send separate from the names of your constants, and join them with an explicit map.

  • Is klass < Formats::Base after const_get enough on its own?
    It rejects foreign classes, but only after the lookup ran, and a lookup can trigger `autoload` or a `const_missing` hook that loads files. It is a useful second check behind a list, not a replacement for one.
  • Does const_get on request strings still risk symbol-table exhaustion?
    Not in current Ruby. Dynamically created symbols are garbage-collected, and Ruby's security guide lists `const_get`, `to_sym` and `respond_to?` as no longer a threat on that front. The remaining risk is what the lookup returns, not memory.

saying these in an interview costs you the question

  • Calling const_get on my own module means only its classes can be returned.
  • Passing false as the inherit flag blocks every top-level constant.
  • const_get returns nil when the name does not exist.
  • Checking the class after const_get is as good as an allowlist.