Skip to content

Commit 622b144

Browse files
etiennebarriebyroot
andcommitted
Add a config.action_controller.json_renderer_escapes framework default
Co-Authored-By: Jean Boussier <[email protected]>
1 parent f0b8238 commit 622b144

File tree

7 files changed

+65
-9
lines changed

7 files changed

+65
-9
lines changed

actionpack/CHANGELOG.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
* The JSON renderer doesn't escape HTML entities or Unicode line separators anymore.
22

3-
Using `render json:` will no longer escape a few characters that can cause errors when the resulting JSON is
4-
embedded in JavaScript, or vulnerabilities when the resulting JSON is embedded in HTML.
3+
Using `render json:` will no longer escape `<`, `>`, `&`, `U+2028` and `U+2029` characters that can cause errors
4+
when the resulting JSON is embedded in JavaScript, or vulnerabilities when the resulting JSON is embedded in HTML.
55

66
Since the renderer is used to return a JSON document as `application/json`, it's typically not necessary to escape
77
those characters, and it improves performance.
88

99
Escaping will still occur when the `:callback` option is set, since the JSON is used as JavaScript code in this
1010
situation (JSONP).
1111

12-
You can use the `:escape` option to restore the escaping behavior.
12+
You can use the `:escape` option or set `config.action_controller.escape_json_responses` to `true` to restore the
13+
escaping behavior.
1314

1415
```ruby
1516
class PostsController < ApplicationController

actionpack/lib/action_controller/metal/renderers.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ module Renderers
2929

3030
included do
3131
class_attribute :_renderers, default: Set.new.freeze
32+
class_attribute :escape_json_responses, instance_accessor: false, default: true
3233
end
3334

3435
# Used in ActionController::Base and ActionController::API to include all
@@ -153,7 +154,7 @@ def _render_to_body_with_renderer(options) # :nodoc:
153154

154155
add :json do |json, options|
155156
json_options = options.except(:callback, :content_type, :status)
156-
json_options[:escape] ||= false unless options[:callback].present?
157+
json_options[:escape] ||= false if !self.class.escape_json_responses? && options[:callback].blank?
157158
json = json.to_json(json_options) unless json.kind_of?(String)
158159

159160
if options[:callback].present?

actionpack/lib/action_controller/railtie.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,5 +133,18 @@ class Railtie < Rails::Railtie # :nodoc:
133133
ActionController::TestCase.executor_around_each_request = app.config.active_support.executor_around_test_case
134134
end
135135
end
136+
137+
initializer "action_controller.escape_json_responses_deprecated_warning" do
138+
config.after_initialize do
139+
ActiveSupport.on_load(:action_controller) do
140+
if ActionController::Base.escape_json_responses
141+
ActionController.deprecator.warn(<<~MSG.squish)
142+
Setting action_controller.escape_json_responses = true is deprecated and will have no effect in Rails 8.2.
143+
Set it to `false` or use `config.load_defaults(8.1)`.
144+
MSG
145+
end
146+
end
147+
end
148+
end
136149
end
137150
end

actionpack/test/controller/render_json_test.rb

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
require "abstract_unit"
44
require "controller/fake_models"
55
require "active_support/logger"
6+
require "active_support/core_ext/object/with"
67

78
class RenderJsonTest < ActionController::TestCase
89
class JsonRenderable
@@ -120,10 +121,12 @@ def test_render_json_with_callback_escapes_js_chars
120121
assert_equal "text/javascript", @response.media_type
121122
end
122123

123-
def test_render_json_without_callback_does_not_escape_js_chars
124-
get :render_json_unsafe_chars_without_callback
125-
assert_equal %({"hello":"\u2028\u2029<script>"}), @response.body
126-
assert_equal "application/json", @response.media_type
124+
def test_render_json_with_new_default_and_without_callback_does_not_escape_js_chars
125+
TestController.with(escape_json_responses: false) do
126+
get :render_json_unsafe_chars_without_callback
127+
assert_equal %({"hello":"\u2028\u2029<script>"}), @response.body
128+
assert_equal "application/json", @response.media_type
129+
end
127130
end
128131

129132
def test_render_json_with_custom_content_type
@@ -157,6 +160,6 @@ def test_render_json_calls_to_json_from_object
157160

158161
def test_render_json_avoids_view_options
159162
get :render_json_inspect_options
160-
assert_equal '{"options":{"escape":false}}', @response.body
163+
assert_equal '{"options":{}}', @response.body
161164
end
162165
end

guides/source/configuring.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ Below are the default values associated with each target version. In cases of co
6060

6161
#### Default Values for Target Version 8.1
6262

63+
- [`config.action_controller.escape_json_responses`](#config-action-controller-escape-json-responses): `false`
6364
- [`config.yjit`](#config-yjit): `!Rails.env.local?`
6465

6566
#### Default Values for Target Version 8.0
@@ -1964,6 +1965,21 @@ The default value depends on the `config.load_defaults` target version:
19641965
Configures the [`ParamsWrapper`](https://api.rubyonrails.org/classes/ActionController/ParamsWrapper.html). This can be called at
19651966
the top level, or on individual controllers.
19661967

1968+
#### `config.action_controller.escape_json_responses`
1969+
1970+
Configures the JSON renderer to escape HTML entities and Unicode characters that are invalid in JavaScript.
1971+
1972+
This is useful if you relied on the JSON response having those characters escaped to embed the JSON document in
1973+
\<script> tags in HTML.
1974+
1975+
This is mainly for compatibility when upgrading Rails applications, otherwise you can use the `:escape` option for
1976+
`render json:` in specific controller actions.
1977+
1978+
| Starting with version | The default value is |
1979+
| --------------------- | -------------------- |
1980+
| (original) | `true` |
1981+
| 8.1 | `false` |
1982+
19671983
### Configuring Action Dispatch
19681984

19691985
#### `config.action_dispatch.cookies_serializer`

railties/lib/rails/application/configuration.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,10 @@ def load_defaults(target_version)
355355
# redefine methods (e.g. mocking), hence YJIT isn't generally
356356
# faster in these environments.
357357
self.yjit = !Rails.env.local?
358+
359+
if respond_to?(:action_controller)
360+
action_controller.escape_json_responses = false
361+
end
358362
else
359363
raise "Unknown version #{target_version.to_s.inspect}"
360364
end

railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_8_1.rb.tt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,21 @@
88
#
99
# Read the Guide for Upgrading Ruby on Rails for more info on each option.
1010
# https://guides.rubyonrails.org/upgrading_ruby_on_rails.html
11+
12+
###
13+
# Skips escaping HTML entities and line separators. When set to `false`, the
14+
# JSON renderer no longer escapes these to improve performance.
15+
#
16+
# Example:
17+
# class PostsController < ApplicationController
18+
# def index
19+
# render json: { key: "\u2028\u2029<>&" }
20+
# end
21+
# end
22+
#
23+
# Renders `{"key":"\u2028\u2029\u003c\u003e\u0026"}` with the previous default, but `{"key":"

<>&"}` with the config
24+
# set to `false`.
25+
#
26+
# Applications that want to keep the escaping behavior can set the config to `true`.
27+
#++
28+
# Rails.configuration.action_controller.escape_json_responses = false

0 commit comments

Comments
 (0)