In a Ruby report builder, why is ERB.new(params[:template]).result dangerous, and how should user-authored report templates be rendered instead?
answer
- a template compiles to Ruby source
- ERB#result calls eval
- default binding from TOPLEVEL_BINDING
- result_with_hash only presets locals
- html_escape stops XSS, not code
basics
~20 sERB compiles a template into Ruby source and ERB#result runs it with eval, so a user-written template can execute any code the app can. Render user templates with placeholder substitution from a fixed hash, or a logic-less template language.
solid answer
~40 s`ERB.new` turns a template into Ruby source, where `<%= %>` becomes an expression whose value is appended and `<% %>` becomes a statement, and `#result` passes that source to `eval` with a binding, by default a fresh one derived from `TOPLEVEL_BINDING`. A template containing `<%= ENV.to_h %>` or `<%= File.read("/etc/passwd") %>` therefore runs with the app's privileges. `result_with_hash` only presets local variables on that binding and evaluates the same way. `ERB::Util.html_escape` protects HTML output from cross-site scripting, a different bug that cannot stop code inside the template. ERB is for templates developers write; for user-authored ones, replace `{{title}}`-style placeholders with `String#gsub` and a fixed hash of values, or use a logic-less template language built for untrusted authors.
code
ruby · 5 linesrequire "erb"
template = "<%= ENV.to_h %>" # imagine this came from the request
ERB.new(template).result # evaluates the template as Ruby: every env var
ERB.new(template).src # shows the generated Ruby sourcego deeper
Remember that ERB templates contain Ruby code in their tags, so a template written by a user is code written by a user.
Explain how ERB compiles tags to Ruby, that result and result_with_hash both call eval, and why html_escape addresses a different bug.
Design user-customisable reports with placeholder substitution or a logic-less engine, and audit where templates are stored and who can edit them.
Decide which roles may author executable templates at all, and keep that power out of tenant-facing features by design.
## How ERB turns text into code **ERB** is Ruby's standard templating library, shipped as a default gem. `ERB.new(template)` compiles the template into a string of Ruby source, which you can inspect through `ERB#src`, and rendering runs that source: | Tag | Compiles to | |---|---| | `<%= expr %>` | an expression whose value is converted to a string and appended to the output | | `<% code %>` | a statement executed in place (loops, conditionals, anything) | | `<%# note %>` | a comment, dropped | | `<%%` | a literal `<%` in the output | `ERB#result(binding)` then calls `eval` on that source. With no argument it builds a new binding based on `TOPLEVEL_BINDING`; `ERB#run` does the same and prints the output; `ERB#result_with_hash(hash)` builds that binding, sets each key as a local variable, and calls `result`. **Every path ends in `eval`.** A template is a program, and whoever writes it is writing Ruby. ## What an attacker's template can do A report builder that accepts a template in the request, `ERB.new(params[:template]).result`, gives the request author the whole language: - `<%= ENV.to_h %>` prints every environment variable, which usually includes database URLs and API keys. - `<%= File.read("/etc/passwd") %>` reads any file the process can. - `<% loop {} %>` pins a worker forever: denial of service with no exploit at all. - `<% Object.const_get(...) %>`, `send` and friends reach any class loaded into the process. ## Why the obvious knobs do not help - **`result_with_hash` is not a sandbox.** It only decides which local variables are preset. Constants such as `File`, `ENV` and `Object` resolve from any binding. - **A custom binding is not a sandbox either.** Rendering against an object's binding changes `self` and the visible locals, but top-level constants remain reachable, for example through `::File`. - **`trim_mode:` does not restrict code.** It controls newline trimming around tags, and its `%` mode even adds a way to write code: lines starting with `%` run as Ruby. - **`ERB::Util.html_escape`** (alias `h`) escapes `&`, `<`, `>`, `"` and `'` in values placed into HTML. That prevents **cross-site scripting** from data, which is a real concern, but it runs on the output of Ruby code, not on the template's code. - **Validating the template with a regex** is the denylist problem again: Ruby has too many ways to spell the same call. ## What to use instead 1. **Placeholder substitution.** Define the fields a user may reference, build a hash of their string values, and replace `{{name}}` tokens with `String#gsub` and `Hash#fetch`. Nothing is parsed as code, and unknown placeholders render empty or raise, as you choose. 2. **A logic-less template language.** Where users need loops and conditionals, use an engine designed for untrusted authors, whose templates are interpreted as data rather than compiled to Ruby. 3. **Keep ERB for developer-authored files.** Templates checked into the repository, where review covers them, are exactly what ERB is for. 4. **Escape values anyway.** When the output is HTML, pass each substituted value through `ERB::Util.html_escape` so a user's report title cannot inject markup. ## Using ERB well where it belongs ERB is the right tool for templates that live in the repository: mailers, generated configuration, code generators. Good habits there: - **Load templates from files you ship**, for example `ERB.new(File.read(path), trim_mode: "-")`, never from a path the request chose. - **Prefer `result_with_hash`** over `result(binding)` so the template sees only the locals you pass, not every local in the calling method. That is hygiene, not a security boundary. - **Escape at the point of output** with `ERB::Util.html_escape` (or its short alias `h`) whenever the output is HTML and a value came from a user. ## Review checklist - `ERB.new` whose argument comes from a request, a database row users can edit, or an uploaded file. - "Safe mode" wrappers that choose a binding or strip tags: they are denylists. - Templates stored by one tenant and rendered for another, which turns code execution into cross-tenant data access as well.
- Is ERB.new safe if you only call it on templates stored in your database?Only if nobody outside the development team can write those rows. A template an admin, tenant or import job can edit is untrusted input with extra steps: anyone who can change the row can run code on every render.
- What does ERB::Util.html_escape protect against in a template?It escapes `&`, `<`, `>`, `"` and `'` in values placed into HTML, so data such as a report title cannot inject markup or scripts. It does nothing about Ruby code in the template itself, because that code runs before any output exists.
saying these in an interview costs you the question
- ERB is only string interpolation, so user templates are harmless.
- Escaping output with ERB::Util.html_escape makes an untrusted template safe.
- result_with_hash limits the template to the keys in the hash.
- Rendering against a custom binding stops a template from reaching File or ENV.