Add "escape json" setting for res.json and res.jsonp

closes #3268
closes #3269
This commit is contained in:
Greg Guthe 2017-04-05 17:42:09 -04:00 committed by Douglas Christopher Wilson
parent 628438d8d8
commit 7154014785
4 changed files with 76 additions and 5 deletions

View File

@ -1,6 +1,7 @@
unreleased
==========
* Add `"json escape"` setting for `res.json` and `res.jsonp`
* Improve error message when autoloading invalid view engine
* Improve error messages when non-function provided as middleware
* Skip `Buffer` encoding when not generating ETag for small response

View File

@ -254,9 +254,10 @@ res.json = function json(obj) {
// settings
var app = this.app;
var escape = app.get('json escape')
var replacer = app.get('json replacer');
var spaces = app.get('json spaces');
var body = stringify(val, replacer, spaces);
var body = stringify(val, replacer, spaces, escape)
// content-type
if (!this.get('Content-Type')) {
@ -296,9 +297,10 @@ res.jsonp = function jsonp(obj) {
// settings
var app = this.app;
var escape = app.get('json escape')
var replacer = app.get('json replacer');
var spaces = app.get('json spaces');
var body = stringify(val, replacer, spaces);
var body = stringify(val, replacer, spaces, escape)
var callback = this.req.query[app.get('jsonp callback name')];
// content-type
@ -1098,14 +1100,38 @@ function sendfile(res, file, options, callback) {
}
/**
* Stringify JSON, like JSON.stringify, but v8 optimized.
* Stringify JSON, like JSON.stringify, but v8 optimized, with the
* ability to escape characters that can trigger HTML sniffing.
*
* @param {*} value
* @param {function} replaces
* @param {number} spaces
* @param {boolean} escape
* @returns {string}
* @private
*/
function stringify(value, replacer, spaces) {
function stringify (value, replacer, spaces, escape) {
// v8 checks arguments.length for optimizing simple call
// https://bugs.chromium.org/p/v8/issues/detail?id=4730
return replacer || spaces
var json = replacer || spaces
? JSON.stringify(value, replacer, spaces)
: JSON.stringify(value);
if (escape) {
json = json.replace(/[<>&]/g, function (c) {
switch (c.charCodeAt(0)) {
case 0x3c:
return '\\u003c'
case 0x3e:
return '\\u003e'
case 0x26:
return '\\u0026'
default:
return c
}
})
}
return json
}

View File

@ -102,6 +102,28 @@ describe('res', function(){
})
})
describe('"json escape" setting', function () {
it('should be undefined by default', function () {
var app = express()
assert.strictEqual(app.get('json escape'), undefined)
})
it('should unicode escape HTML-sniffing characters', function (done) {
var app = express()
app.enable('json escape')
app.use(function (req, res) {
res.json({ '&': '<script>' })
})
request(app)
.get('/')
.expect('Content-Type', 'application/json; charset=utf-8')
.expect(200, '{"\\u0026":"\\u003cscript\\u003e"}', done)
})
})
describe('"json replacer" setting', function(){
it('should be passed to JSON.stringify()', function(done){
var app = express();

View File

@ -242,6 +242,28 @@ describe('res', function(){
})
})
describe('"json escape" setting', function () {
it('should be undefined by default', function () {
var app = express()
assert.strictEqual(app.get('json escape'), undefined)
})
it('should unicode escape HTML-sniffing characters', function (done) {
var app = express()
app.enable('json escape')
app.use(function (req, res) {
res.jsonp({ '&': '\u2028<script>\u2029' })
})
request(app)
.get('/?callback=foo')
.expect('Content-Type', 'text/javascript; charset=utf-8')
.expect(200, /foo\({"\\u0026":"\\u2028\\u003cscript\\u003e\\u2029"}\)/, done)
})
})
describe('"json replacer" setting', function(){
it('should be passed to JSON.stringify()', function(done){
var app = express();