Skip to content

Commit 7154014

Browse files
Greg Guthedougwilson
Greg Guthe
authored andcommitted
Add "escape json" setting for res.json and res.jsonp
closes #3268 closes #3269
1 parent 628438d commit 7154014

File tree

4 files changed

+76
-5
lines changed

4 files changed

+76
-5
lines changed

History.md

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
unreleased
22
==========
33

4+
* Add `"json escape"` setting for `res.json` and `res.jsonp`
45
* Improve error message when autoloading invalid view engine
56
* Improve error messages when non-function provided as middleware
67
* Skip `Buffer` encoding when not generating ETag for small response

lib/response.js

+31-5
Original file line numberDiff line numberDiff line change
@@ -254,9 +254,10 @@ res.json = function json(obj) {
254254

255255
// settings
256256
var app = this.app;
257+
var escape = app.get('json escape')
257258
var replacer = app.get('json replacer');
258259
var spaces = app.get('json spaces');
259-
var body = stringify(val, replacer, spaces);
260+
var body = stringify(val, replacer, spaces, escape)
260261

261262
// content-type
262263
if (!this.get('Content-Type')) {
@@ -296,9 +297,10 @@ res.jsonp = function jsonp(obj) {
296297

297298
// settings
298299
var app = this.app;
300+
var escape = app.get('json escape')
299301
var replacer = app.get('json replacer');
300302
var spaces = app.get('json spaces');
301-
var body = stringify(val, replacer, spaces);
303+
var body = stringify(val, replacer, spaces, escape)
302304
var callback = this.req.query[app.get('jsonp callback name')];
303305

304306
// content-type
@@ -1098,14 +1100,38 @@ function sendfile(res, file, options, callback) {
10981100
}
10991101

11001102
/**
1101-
* Stringify JSON, like JSON.stringify, but v8 optimized.
1103+
* Stringify JSON, like JSON.stringify, but v8 optimized, with the
1104+
* ability to escape characters that can trigger HTML sniffing.
1105+
*
1106+
* @param {*} value
1107+
* @param {function} replaces
1108+
* @param {number} spaces
1109+
* @param {boolean} escape
1110+
* @returns {string}
11021111
* @private
11031112
*/
11041113

1105-
function stringify(value, replacer, spaces) {
1114+
function stringify (value, replacer, spaces, escape) {
11061115
// v8 checks arguments.length for optimizing simple call
11071116
// https://bugs.chromium.org/p/v8/issues/detail?id=4730
1108-
return replacer || spaces
1117+
var json = replacer || spaces
11091118
? JSON.stringify(value, replacer, spaces)
11101119
: JSON.stringify(value);
1120+
1121+
if (escape) {
1122+
json = json.replace(/[<>&]/g, function (c) {
1123+
switch (c.charCodeAt(0)) {
1124+
case 0x3c:
1125+
return '\\u003c'
1126+
case 0x3e:
1127+
return '\\u003e'
1128+
case 0x26:
1129+
return '\\u0026'
1130+
default:
1131+
return c
1132+
}
1133+
})
1134+
}
1135+
1136+
return json
11111137
}

test/res.json.js

+22
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,28 @@ describe('res', function(){
102102
})
103103
})
104104

105+
describe('"json escape" setting', function () {
106+
it('should be undefined by default', function () {
107+
var app = express()
108+
assert.strictEqual(app.get('json escape'), undefined)
109+
})
110+
111+
it('should unicode escape HTML-sniffing characters', function (done) {
112+
var app = express()
113+
114+
app.enable('json escape')
115+
116+
app.use(function (req, res) {
117+
res.json({ '&': '<script>' })
118+
})
119+
120+
request(app)
121+
.get('/')
122+
.expect('Content-Type', 'application/json; charset=utf-8')
123+
.expect(200, '{"\\u0026":"\\u003cscript\\u003e"}', done)
124+
})
125+
})
126+
105127
describe('"json replacer" setting', function(){
106128
it('should be passed to JSON.stringify()', function(done){
107129
var app = express();

test/res.jsonp.js

+22
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,28 @@ describe('res', function(){
242242
})
243243
})
244244

245+
describe('"json escape" setting', function () {
246+
it('should be undefined by default', function () {
247+
var app = express()
248+
assert.strictEqual(app.get('json escape'), undefined)
249+
})
250+
251+
it('should unicode escape HTML-sniffing characters', function (done) {
252+
var app = express()
253+
254+
app.enable('json escape')
255+
256+
app.use(function (req, res) {
257+
res.jsonp({ '&': '\u2028<script>\u2029' })
258+
})
259+
260+
request(app)
261+
.get('/?callback=foo')
262+
.expect('Content-Type', 'text/javascript; charset=utf-8')
263+
.expect(200, /foo\({"\\u0026":"\\u2028\\u003cscript\\u003e\\u2029"}\)/, done)
264+
})
265+
})
266+
245267
describe('"json replacer" setting', function(){
246268
it('should be passed to JSON.stringify()', function(done){
247269
var app = express();

0 commit comments

Comments
 (0)