Skip to content

Linter Rule: Disallow unsafe ERB output inside <script> tags ​

Rule: erb-no-unsafe-script-interpolation

Description ​

ERB interpolation in <script> tags must use .to_json to safely serialize Ruby data into JavaScript. Without .to_json, user-controlled values can break out of string literals and execute arbitrary JavaScript.

This rule also detects usage of j() and escape_javascript() inside <script> tags and recommends .to_json instead, because j() is only safe when the output is placed inside quoted string literals, a subtle requirement that is easy to get wrong.

Rationale ​

The main goal of this rule is to assert that Ruby data translates into JavaScript data, but never becomes JavaScript code. ERB output inside <script> tags is interpolated directly into the JavaScript context. Without proper serialization, an attacker can inject arbitrary JavaScript by manipulating the interpolated value.

For example, consider:

erb
<script>
Missing a `nonce` attribute on `<script>` tag. Use `request.content_security_policy_nonce`. (html-require-script-nonce)
var name = "<%= user.name %>";
Unsafe ERB output in `<script>` tag. Use `.to_json` to safely serialize values into JavaScript. (erb-no-unsafe-script-interpolation)
</script>

If user.name contains "; alert(1); ", it renders as:

html
<script>
Missing a `nonce` attribute on `<script>` tag. Use `request.content_security_policy_nonce`. (html-require-script-nonce)
var name = ""; alert(1); ""; </script>

This is a Cross-Site Scripting (XSS) vulnerability, as the attacker breaks out of the string literal and executes arbitrary JavaScript.

Using .to_json properly escapes the value and wraps it in quotes:

erb
<script>
Missing a `nonce` attribute on `<script>` tag. Use `request.content_security_policy_nonce`. (html-require-script-nonce)
var name = <%= user.name.to_json %>; </script>

With the same malicious input "; alert(1); ", .to_json safely renders:

html
<script>
Missing a `nonce` attribute on `<script>` tag. Use `request.content_security_policy_nonce`. (html-require-script-nonce)
var name = "\"; alert(1); \""; </script>

The value stays contained as a string, and no code is executed.

Why not j() or escape_javascript()? ​

j() escapes characters special inside JavaScript string literals (quotes, newlines, etc.), but it does not produce a quoted value. This means it's only safe when wrapped in quotes.

This works, but is fragile. In this example safety depends on the surrounding quotes:

erb
<script>
Missing a `nonce` attribute on `<script>` tag. Use `request.content_security_policy_nonce`. (html-require-script-nonce)
var name = '<%= j user.name %>';
Avoid `j()` / `escape_javascript()` in `<script>` tags. It is only safe inside quoted string literals. Use `.to_json` instead, which is safe in any position. (erb-no-unsafe-script-interpolation)
</script>

Without quotes, j() provides no protection and is UNSAFE, so code can still be injected:

erb
<script>
Missing a `nonce` attribute on `<script>` tag. Use `request.content_security_policy_nonce`. (html-require-script-nonce)
var name = <%= j user.name %>;
Avoid `j()` / `escape_javascript()` in `<script>` tags. It is only safe inside quoted string literals. Use `.to_json` instead, which is safe in any position. (erb-no-unsafe-script-interpolation)
</script>

If user.name is alert(1), j() passes it through unchanged (no special characters to escape), rendering:

html
<script>
Missing a `nonce` attribute on `<script>` tag. Use `request.content_security_policy_nonce`. (html-require-script-nonce)
var name = alert(1); </script>

This results in a Cross-Site Scripting (XSS) vulnerability, as the attacker-controlled value is interpreted as JavaScript code rather than a string/data.

.to_json is safe in any position because it always produces a valid, quoted JavaScript value.

Examples ​

✅ Good ​

erb
<script>
Missing a `nonce` attribute on `<script>` tag. Use `request.content_security_policy_nonce`. (html-require-script-nonce)
var name = <%= user.name.to_json %>; </script>
erb
<script>
Missing a `nonce` attribute on `<script>` tag. Use `request.content_security_policy_nonce`. (html-require-script-nonce)
var data = <%== config.to_json %>; </script>
erb
<script>
Missing a `nonce` attribute on `<script>` tag. Use `request.content_security_policy_nonce`. (html-require-script-nonce)
<%= raw unsafe.to_json %> </script>

🚫 Bad ​

erb
<script>
Missing a `nonce` attribute on `<script>` tag. Use `request.content_security_policy_nonce`. (html-require-script-nonce)
var name = "<%= user.name %>";
Unsafe ERB output in `<script>` tag. Use `.to_json` to safely serialize values into JavaScript. (erb-no-unsafe-script-interpolation)
</script>
erb
<script>
Missing a `nonce` attribute on `<script>` tag. Use `request.content_security_policy_nonce`. (html-require-script-nonce)
if (a < 1) { <%= unsafe %> }
Unsafe ERB output in `<script>` tag. Use `.to_json` to safely serialize values into JavaScript. (erb-no-unsafe-script-interpolation)
</script>
erb
<script>
Missing a `nonce` attribute on `<script>` tag. Use `request.content_security_policy_nonce`. (html-require-script-nonce)
<%= @feature.html_safe %>
Unsafe ERB output in `<script>` tag. Use `.to_json` to safely serialize values into JavaScript. (erb-no-unsafe-script-interpolation)
</script>

⚠️ Prefer .to_json over j() / escape_javascript() ​

diff
- const url = '<%= j @my_path %>';
+ const url = <%= @my_path.to_json %>;
diff
- const name = '<%= escape_javascript(user.name) %>';
+ const name = <%= user.name.to_json %>;

References ​

Released under the MIT License.