Skip to content

Commit f0b8238

Browse files
etiennebarriebyroot
andcommitted
Don't always escape JSON when calling render json:
When `render json:` is used in a controller, and there's no callback option (used for JSONP), the resulting JSON document doesn't need to be HTML-safe (no need to escape HTML entities) or embeddable into JavaScript (no need to escape U+2028 and U+2029). This both saves a costly operation and renders cleaner JSON. Co-authored-by: Jean Boussier <[email protected]>
1 parent dd44466 commit f0b8238

File tree

3 files changed

+47
-3
lines changed

3 files changed

+47
-3
lines changed

actionpack/CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,26 @@
1+
* The JSON renderer doesn't escape HTML entities or Unicode line separators anymore.
2+
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.
5+
6+
Since the renderer is used to return a JSON document as `application/json`, it's typically not necessary to escape
7+
those characters, and it improves performance.
8+
9+
Escaping will still occur when the `:callback` option is set, since the JSON is used as JavaScript code in this
10+
situation (JSONP).
11+
12+
You can use the `:escape` option to restore the escaping behavior.
13+
14+
```ruby
15+
class PostsController < ApplicationController
16+
def index
17+
render json: Post.last(30), escape: true
18+
end
19+
end
20+
```
21+
22+
*Étienne Barrié*, *Jean Boussier*
23+
124
* Load lazy route sets before inserting test routes
225

326
Without loading lazy route sets early, we miss `after_routes_loaded` callbacks, or risk

actionpack/lib/action_controller/metal/renderers.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def self.remove(key)
8686
remove_possible_method(method_name)
8787
end
8888

89-
def self._render_with_renderer_method_name(key)
89+
def self._render_with_renderer_method_name(key) # :nodoc:
9090
"_render_with_renderer_#{key}"
9191
end
9292

@@ -140,7 +140,7 @@ def render_to_body(options)
140140
_render_to_body_with_renderer(options) || super
141141
end
142142

143-
def _render_to_body_with_renderer(options)
143+
def _render_to_body_with_renderer(options) # :nodoc:
144144
_renderers.each do |name|
145145
if options.key?(name)
146146
_process_options(options)
@@ -153,6 +153,7 @@ def _render_to_body_with_renderer(options)
153153

154154
add :json do |json, options|
155155
json_options = options.except(:callback, :content_type, :status)
156+
json_options[:escape] ||= false unless options[:callback].present?
156157
json = json.to_json(json_options) unless json.kind_of?(String)
157158

158159
if options[:callback].present?

actionpack/test/controller/render_json_test.rb

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ def render_json_hello_world_with_callback
4646
render json: ActiveSupport::JSON.encode(hello: "world"), callback: "alert"
4747
end
4848

49+
def render_json_unsafe_chars_with_callback
50+
render json: { hello: "\u2028\u2029<script>" }, callback: "alert"
51+
end
52+
53+
def render_json_unsafe_chars_without_callback
54+
render json: { hello: "\u2028\u2029<script>" }
55+
end
56+
4957
def render_json_with_custom_content_type
5058
render json: ActiveSupport::JSON.encode(hello: "world"), content_type: "text/javascript"
5159
end
@@ -106,6 +114,18 @@ def test_render_json_with_callback
106114
assert_equal "text/javascript", @response.media_type
107115
end
108116

117+
def test_render_json_with_callback_escapes_js_chars
118+
get :render_json_unsafe_chars_with_callback, xhr: true
119+
assert_equal '/**/alert({"hello":"\\u2028\\u2029\\u003cscript\\u003e"})', @response.body
120+
assert_equal "text/javascript", @response.media_type
121+
end
122+
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
127+
end
128+
109129
def test_render_json_with_custom_content_type
110130
get :render_json_with_custom_content_type, xhr: true
111131
assert_equal '{"hello":"world"}', @response.body
@@ -137,6 +157,6 @@ def test_render_json_calls_to_json_from_object
137157

138158
def test_render_json_avoids_view_options
139159
get :render_json_inspect_options
140-
assert_equal '{"options":{}}', @response.body
160+
assert_equal '{"options":{"escape":false}}', @response.body
141161
end
142162
end

0 commit comments

Comments
 (0)