From f449444fe215c5b0d23b93b7416ffb88e726d229 Mon Sep 17 00:00:00 2001 From: emilANS Date: Sun, 15 Mar 2026 00:27:23 -0500 Subject: [PATCH 01/10] Fix issue #5995: cookies encryption added --- examples/cookies/index.js | 73 ++++-- lib/response.js | 533 +++++++++++++++++++++++--------------- package.json | 1 + 3 files changed, 373 insertions(+), 234 deletions(-) diff --git a/examples/cookies/index.js b/examples/cookies/index.js index 0620cb40e45..5a9d7da688f 100644 --- a/examples/cookies/index.js +++ b/examples/cookies/index.js @@ -1,53 +1,86 @@ -'use strict' +"use strict"; /** * Module dependencies. */ -var express = require('../../'); -var app = module.exports = express(); -var logger = require('morgan'); -var cookieParser = require('cookie-parser'); +var express = require("../../"); +var app = (module.exports = express()); +var logger = require("morgan"); +var cookieParser = require("cookie-parser"); +var crypto = require("node:crypto"); // custom log format -if (process.env.NODE_ENV !== 'test') app.use(logger(':method :url')) +if (process.env.NODE_ENV !== "test") app.use(logger(":method :url")); // parses request cookies, populating // req.cookies and req.signedCookies // when the secret is passed, used // for signing the cookies. -app.use(cookieParser('my secret here')); +app.use(cookieParser("my secret here")); + +app.use(express.json()); // parses x-www-form-urlencoded -app.use(express.urlencoded()) +app.use(express.urlencoded()); -app.get('/', function(req, res){ +app.get("/", function (req, res) { if (req.cookies.remember) { - res.send('Remembered :). Click to forget!.'); + res.send( + 'Remembered :). Click to forget!.

', + ); } else { - res.send('

Check to ' - + '.

'); + res.send( + '

Check to ' + + '.' + + "

", + ); } }); -app.get('/forget', function(req, res){ - res.clearCookie('remember'); - res.redirect(req.get('Referrer') || '/'); +app.get("/forget", function (req, res) { + res.clearCookie("remember"); + res.redirect(req.get("Referrer") || "/"); }); -app.post('/', function(req, res){ +app.post("/", function (req, res) { var minute = 60000; if (req.body && req.body.remember) { - res.cookie('remember', 1, { maxAge: minute }) + res.cookie("remember", 1, { maxAge: minute }); } - res.redirect(req.get('Referrer') || '/'); + res.redirect(req.get("Referrer") || "/"); +}); + +app.post("/encryptCookies", function (req, res) { + const iv = crypto.randomBytes(16); + + const encryptionAlgorithm = "aes-256-cbc"; + + const hashAlgorithm = "sha256"; + + res.cookie( + "encryptedCookie", + "i like to hide my cookies under the sofa", + {signed: false}, + { encryptionAlgorithm, hashAlgorithm, iv }, + ); + + res.send("cookie encrypted"); +}); + +app.post("/decryptCookies", function (req, res) { + const encryptedCookie = req.cookies.encryptedCookie; + + const decryptedCookie = res.decryptCookie(encryptedCookie); + + res.send(decryptedCookie); }); /* istanbul ignore next */ if (!module.parent) { app.listen(3000); - console.log('Express started on port 3000'); + console.log("Express started on port 3000"); } diff --git a/lib/response.js b/lib/response.js index f965e539dd2..fe83c48efd6 100644 --- a/lib/response.js +++ b/lib/response.js @@ -5,48 +5,48 @@ * MIT Licensed */ -'use strict'; +"use strict"; /** * Module dependencies. * @private */ -var contentDisposition = require('content-disposition'); -var createError = require('http-errors') -var deprecate = require('depd')('express'); -var encodeUrl = require('encodeurl'); -var escapeHtml = require('escape-html'); -var http = require('node:http'); -var onFinished = require('on-finished'); -var mime = require('mime-types') -var path = require('node:path'); -var pathIsAbsolute = require('node:path').isAbsolute; -var statuses = require('statuses') -var sign = require('cookie-signature').sign; -var normalizeType = require('./utils').normalizeType; -var normalizeTypes = require('./utils').normalizeTypes; -var setCharset = require('./utils').setCharset; -var cookie = require('cookie'); -var send = require('send'); +var contentDisposition = require("content-disposition"); +var createError = require("http-errors"); +var deprecate = require("depd")("express"); +var encodeUrl = require("encodeurl"); +var escapeHtml = require("escape-html"); +var http = require("node:http"); +var onFinished = require("on-finished"); +var mime = require("mime-types"); +var path = require("node:path"); +var pathIsAbsolute = require("node:path").isAbsolute; +var statuses = require("statuses"); +var sign = require("cookie-signature").sign; +var normalizeType = require("./utils").normalizeType; +var normalizeTypes = require("./utils").normalizeTypes; +var setCharset = require("./utils").setCharset; +var cookie = require("cookie"); +var send = require("send"); var extname = path.extname; var resolve = path.resolve; -var vary = require('vary'); -const { Buffer } = require('node:buffer'); - +var vary = require("vary"); +var crypto = require("crypto"); +const { Buffer } = require("node:buffer"); /** * Response prototype. * @public */ -var res = Object.create(http.ServerResponse.prototype) +var res = Object.create(http.ServerResponse.prototype); /** * Module exports. * @public */ -module.exports = res +module.exports = res; /** * Set the HTTP status code for the response. @@ -64,11 +64,15 @@ module.exports = res res.status = function status(code) { // Check if the status code is not an integer if (!Number.isInteger(code)) { - throw new TypeError(`Invalid status code: ${JSON.stringify(code)}. Status code must be an integer.`); + throw new TypeError( + `Invalid status code: ${JSON.stringify(code)}. Status code must be an integer.`, + ); } // Check if the status code is outside of Node's valid range if (code < 100 || code > 999) { - throw new RangeError(`Invalid status code: ${JSON.stringify(code)}. Status code must be greater than 99 and less than 1000.`); + throw new RangeError( + `Invalid status code: ${JSON.stringify(code)}. Status code must be greater than 99 and less than 1000.`, + ); } this.statusCode = code; @@ -94,19 +98,27 @@ res.status = function status(code) { * @public */ -res.links = function(links) { - var link = this.get('Link') || ''; - if (link) link += ', '; - return this.set('Link', link + Object.keys(links).map(function(rel) { - // Allow multiple links if links[rel] is an array - if (Array.isArray(links[rel])) { - return links[rel].map(function (singleLink) { - return `<${singleLink}>; rel="${rel}"`; - }).join(', '); - } else { - return `<${links[rel]}>; rel="${rel}"`; - } - }).join(', ')); +res.links = function (links) { + var link = this.get("Link") || ""; + if (link) link += ", "; + return this.set( + "Link", + link + + Object.keys(links) + .map(function (rel) { + // Allow multiple links if links[rel] is an array + if (Array.isArray(links[rel])) { + return links[rel] + .map(function (singleLink) { + return `<${singleLink}>; rel="${rel}"`; + }) + .join(", "); + } else { + return `<${links[rel]}>; rel="${rel}"`; + } + }) + .join(", "), + ); }; /** @@ -132,24 +144,24 @@ res.send = function send(body) { switch (typeof chunk) { // string defaulting to html - case 'string': - encoding = 'utf8'; - const type = this.get('Content-Type'); + case "string": + encoding = "utf8"; + const type = this.get("Content-Type"); - if (typeof type === 'string') { - this.set('Content-Type', setCharset(type, 'utf-8')); + if (typeof type === "string") { + this.set("Content-Type", setCharset(type, "utf-8")); } else { - this.type('html'); + this.type("html"); } break; - case 'boolean': - case 'number': - case 'object': + case "boolean": + case "number": + case "object": if (chunk === null) { - chunk = ''; + chunk = ""; } else if (ArrayBuffer.isView(chunk)) { - if (!this.get('Content-Type')) { - this.type('bin'); + if (!this.get("Content-Type")) { + this.type("bin"); } } else { return this.json(chunk); @@ -158,33 +170,33 @@ res.send = function send(body) { } // determine if ETag should be generated - var etagFn = app.get('etag fn') - var generateETag = !this.get('ETag') && typeof etagFn === 'function' + var etagFn = app.get("etag fn"); + var generateETag = !this.get("ETag") && typeof etagFn === "function"; // populate Content-Length - var len + var len; if (chunk !== undefined) { if (Buffer.isBuffer(chunk)) { // get length of Buffer - len = chunk.length + len = chunk.length; } else if (!generateETag && chunk.length < 1000) { // just calculate length when no ETag + small chunk - len = Buffer.byteLength(chunk, encoding) + len = Buffer.byteLength(chunk, encoding); } else { // convert chunk to Buffer and calculate - chunk = Buffer.from(chunk, encoding) + chunk = Buffer.from(chunk, encoding); encoding = undefined; - len = chunk.length + len = chunk.length; } - this.set('Content-Length', len); + this.set("Content-Length", len); } // populate ETag var etag; if (generateETag && len !== undefined) { if ((etag = etagFn(chunk, encoding))) { - this.set('ETag', etag); + this.set("ETag", etag); } } @@ -193,20 +205,20 @@ res.send = function send(body) { // strip irrelevant headers if (204 === this.statusCode || 304 === this.statusCode) { - this.removeHeader('Content-Type'); - this.removeHeader('Content-Length'); - this.removeHeader('Transfer-Encoding'); - chunk = ''; + this.removeHeader("Content-Type"); + this.removeHeader("Content-Length"); + this.removeHeader("Transfer-Encoding"); + chunk = ""; } // alter headers for 205 if (this.statusCode === 205) { - this.set('Content-Length', '0') - this.removeHeader('Transfer-Encoding') - chunk = '' + this.set("Content-Length", "0"); + this.removeHeader("Transfer-Encoding"); + chunk = ""; } - if (req.method === 'HEAD') { + if (req.method === "HEAD") { // skip body for HEAD this.end(); } else { @@ -232,14 +244,14 @@ res.send = function send(body) { 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(obj, replacer, spaces, escape) + var escape = app.get("json escape"); + var replacer = app.get("json replacer"); + var spaces = app.get("json spaces"); + var body = stringify(obj, replacer, spaces, escape); // content-type - if (!this.get('Content-Type')) { - this.set('Content-Type', 'application/json'); + if (!this.get("Content-Type")) { + this.set("Content-Type", "application/json"); } return this.send(body); @@ -260,16 +272,16 @@ res.json = function json(obj) { 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(obj, replacer, spaces, escape) - var callback = this.req.query[app.get('jsonp callback name')]; + var escape = app.get("json escape"); + var replacer = app.get("json replacer"); + var spaces = app.get("json spaces"); + var body = stringify(obj, replacer, spaces, escape); + var callback = this.req.query[app.get("jsonp callback name")]; // content-type - if (!this.get('Content-Type')) { - this.set('X-Content-Type-Options', 'nosniff'); - this.set('Content-Type', 'application/json'); + if (!this.get("Content-Type")) { + this.set("X-Content-Type-Options", "nosniff"); + this.set("Content-Type", "application/json"); } // fixup callback @@ -278,26 +290,31 @@ res.jsonp = function jsonp(obj) { } // jsonp - if (typeof callback === 'string' && callback.length !== 0) { - this.set('X-Content-Type-Options', 'nosniff'); - this.set('Content-Type', 'text/javascript'); + if (typeof callback === "string" && callback.length !== 0) { + this.set("X-Content-Type-Options", "nosniff"); + this.set("Content-Type", "text/javascript"); // restrict callback charset - callback = callback.replace(/[^\[\]\w$.]/g, ''); + callback = callback.replace(/[^\[\]\w$.]/g, ""); if (body === undefined) { // empty argument - body = '' - } else if (typeof body === 'string') { + body = ""; + } else if (typeof body === "string") { // replace chars not allowed in JavaScript that are in JSON - body = body - .replace(/\u2028/g, '\\u2028') - .replace(/\u2029/g, '\\u2029') + body = body.replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029"); } // the /**/ is a specific security mitigation for "Rosetta Flash JSONP abuse" // the typeof check is just to reduce client error noise - body = '/**/ typeof ' + callback + ' === \'function\' && ' + callback + '(' + body + ');'; + body = + "/**/ typeof " + + callback + + " === 'function' && " + + callback + + "(" + + body + + ");"; } return this.send(body); @@ -319,10 +336,10 @@ res.jsonp = function jsonp(obj) { */ res.sendStatus = function sendStatus(statusCode) { - var body = statuses.message[statusCode] || String(statusCode) + var body = statuses.message[statusCode] || String(statusCode); this.status(statusCode); - this.type('txt'); + this.type("txt"); return this.send(body); }; @@ -376,37 +393,39 @@ res.sendFile = function sendFile(path, options, callback) { var opts = options || {}; if (!path) { - throw new TypeError('path argument is required to res.sendFile'); + throw new TypeError("path argument is required to res.sendFile"); } - if (typeof path !== 'string') { - throw new TypeError('path must be a string to res.sendFile') + if (typeof path !== "string") { + throw new TypeError("path must be a string to res.sendFile"); } // support function as second arg - if (typeof options === 'function') { + if (typeof options === "function") { done = options; opts = {}; } if (!opts.root && !pathIsAbsolute(path)) { - throw new TypeError('path must be absolute or specify root to res.sendFile'); + throw new TypeError( + "path must be absolute or specify root to res.sendFile", + ); } // create file stream var pathname = encodeURI(path); // wire application etag option to send - opts.etag = this.app.enabled('etag'); + opts.etag = this.app.enabled("etag"); var file = send(req, pathname, opts); // transfer sendfile(res, file, opts, function (err) { if (done) return done(err); - if (err && err.code === 'EISDIR') return next(); + if (err && err.code === "EISDIR") return next(); // next() all but write errors - if (err && err.code !== 'ECONNABORTED' && err.syscall !== 'write') { + if (err && err.code !== "ECONNABORTED" && err.syscall !== "write") { next(err); } }); @@ -430,55 +449,55 @@ res.sendFile = function sendFile(path, options, callback) { * @public */ -res.download = function download (path, filename, options, callback) { +res.download = function download(path, filename, options, callback) { var done = callback; var name = filename; - var opts = options || null + var opts = options || null; // support function as second or third arg - if (typeof filename === 'function') { + if (typeof filename === "function") { done = filename; name = null; - opts = null - } else if (typeof options === 'function') { - done = options - opts = null + opts = null; + } else if (typeof options === "function") { + done = options; + opts = null; } // support optional filename, where options may be in it's place - if (typeof filename === 'object' && - (typeof options === 'function' || options === undefined)) { - name = null - opts = filename + if ( + typeof filename === "object" && + (typeof options === "function" || options === undefined) + ) { + name = null; + opts = filename; } // set Content-Disposition when file is sent var headers = { - 'Content-Disposition': contentDisposition(name || path) + "Content-Disposition": contentDisposition(name || path), }; // merge user-provided headers if (opts && opts.headers) { - var keys = Object.keys(opts.headers) + var keys = Object.keys(opts.headers); for (var i = 0; i < keys.length; i++) { - var key = keys[i] - if (key.toLowerCase() !== 'content-disposition') { - headers[key] = opts.headers[key] + var key = keys[i]; + if (key.toLowerCase() !== "content-disposition") { + headers[key] = opts.headers[key]; } } } // merge user-provided options - opts = Object.create(opts) - opts.headers = headers + opts = Object.create(opts); + opts.headers = headers; // Resolve the full path for sendFile - var fullPath = !opts.root - ? resolve(path) - : path + var fullPath = !opts.root ? resolve(path) : path; // send file - return this.sendFile(fullPath, opts, done) + return this.sendFile(fullPath, opts, done); }; /** @@ -500,13 +519,13 @@ res.download = function download (path, filename, options, callback) { * @public */ -res.contentType = -res.type = function contentType(type) { - var ct = type.indexOf('/') === -1 - ? (mime.contentType(type) || 'application/octet-stream') - : type; +res.contentType = res.type = function contentType(type) { + var ct = + type.indexOf("/") === -1 + ? mime.contentType(type) || "application/octet-stream" + : type; - return this.set('Content-Type', ct); + return this.set("Content-Type", ct); }; /** @@ -566,28 +585,31 @@ res.type = function contentType(type) { * @public */ -res.format = function(obj){ +res.format = function (obj) { var req = this.req; var next = req.next; - var keys = Object.keys(obj) - .filter(function (v) { return v !== 'default' }) + var keys = Object.keys(obj).filter(function (v) { + return v !== "default"; + }); - var key = keys.length > 0 - ? req.accepts(keys) - : false; + var key = keys.length > 0 ? req.accepts(keys) : false; this.vary("Accept"); if (key) { - this.set('Content-Type', normalizeType(key).value); + this.set("Content-Type", normalizeType(key).value); obj[key](req, this, next); } else if (obj.default) { - obj.default(req, this, next) + obj.default(req, this, next); } else { - next(createError(406, { - types: normalizeTypes(keys).map(function (o) { return o.value }) - })) + next( + createError(406, { + types: normalizeTypes(keys).map(function (o) { + return o.value; + }), + }), + ); } return this; @@ -606,7 +628,7 @@ res.attachment = function attachment(filename) { this.type(extname(filename)); } - this.set('Content-Disposition', contentDisposition(filename)); + this.set("Content-Disposition", contentDisposition(filename)); return this; }; @@ -632,9 +654,11 @@ res.append = function append(field, val) { if (prev) { // concat the new and prev vals - value = Array.isArray(prev) ? prev.concat(val) - : Array.isArray(val) ? [prev].concat(val) - : [prev, val] + value = Array.isArray(prev) + ? prev.concat(val) + : Array.isArray(val) + ? [prev].concat(val) + : [prev, val]; } return this.set(field, value); @@ -661,19 +685,16 @@ res.append = function append(field, val) { * @public */ -res.set = -res.header = function header(field, val) { +res.set = res.header = function header(field, val) { if (arguments.length === 2) { - var value = Array.isArray(val) - ? val.map(String) - : String(val); + var value = Array.isArray(val) ? val.map(String) : String(val); // add charset to content-type - if (field.toLowerCase() === 'content-type') { + if (field.toLowerCase() === "content-type") { if (Array.isArray(value)) { - throw new TypeError('Content-Type cannot be set to an Array'); + throw new TypeError("Content-Type cannot be set to an Array"); } - value = mime.contentType(value) + value = mime.contentType(value); } this.setHeader(field, value); @@ -693,7 +714,7 @@ res.header = function header(field, val) { * @public */ -res.get = function(field){ +res.get = function (field) { return this.getHeader(field); }; @@ -708,11 +729,11 @@ res.get = function(field){ res.clearCookie = function clearCookie(name, options) { // Force cookie expiration by setting expires to the past - const opts = { path: '/', ...options, expires: new Date(1)}; + const opts = { path: "/", ...options, expires: new Date(1) }; // ensure maxAge is not passed - delete opts.maxAge + delete opts.maxAge; - return this.cookie(name, '', opts); + return this.cookie(name, "", opts); }; /** @@ -732,14 +753,24 @@ res.clearCookie = function clearCookie(name, options) { * // same as above * res.cookie('rememberme', '1', { maxAge: 900000, httpOnly: true }) * + * Encrypt: + * - `encryptionAlgorithm` encryption algorithm you will use + * - `hashAlgorithm` hash algorithm you will use + * - `iv` Initialization Vector used for encryption is recommended you create a entropied random value + * + * Examples: + * // Create an encrypted cookie + * res.cookie('encryptedCookie', 'secret thing to be encrypted', {signed = false}, {encryptionAlgorithm: 'aes-256-cbc', hashAlgorithm: 'sha256', iv: crypto.randomBytes(16) }) + * * @param {String} name * @param {String|Object} value * @param {Object} [options] + * @param {{encryptionAlgorithm: string, hashAlgorithm: string, iv: Buffer}} encrypt * @return {ServerResponse} for chaining * @public */ -res.cookie = function (name, value, options) { +res.cookie = function (name, value, options, encrypt) { var opts = { ...options }; var secret = this.req.secret; var signed = opts.signed; @@ -748,32 +779,96 @@ res.cookie = function (name, value, options) { throw new Error('cookieParser("secret") required for signed cookies'); } - var val = typeof value === 'object' - ? 'j:' + JSON.stringify(value) - : String(value); + var val = + typeof value === "object" ? "j:" + JSON.stringify(value) : String(value); + + if (signed && !encrypt) { + val = "s:" + sign(val, secret); + } + + if (!signed && encrypt) { + var encryptionAlgorithm = encrypt.encryptionAlgorithm; + + var hashAlgorithm = encrypt.hashAlgorithm; + + var iv = encrypt.iv; + + const plainText = Buffer.from(JSON.stringify(value), "utf8"); + + const key = crypto + .createHash(hashAlgorithm) + .update(String(secret)) + .digest() + .subarray(0, 32); + + let cipher = crypto.createCipheriv(encryptionAlgorithm, key, iv); + + const encryptedText = Buffer.concat([ + cipher.update(plainText), + cipher.final(), + ]); + + const encryptedTextObject = { + encryptedText: encryptedText, + encryptionAlgorithm: encryptionAlgorithm, + hashAlgorithm: hashAlgorithm, + iv: iv, + key: key, + }; - if (signed) { - val = 's:' + sign(val, secret); + val = JSON.stringify(encryptedTextObject); + } + + if (signed && encrypt) { + throw new Error( + "You should decide between a signed cookie or a encrypted cookie, you have signed = true and passing to encrypt parameters", + ); } if (opts.maxAge != null) { - var maxAge = opts.maxAge - 0 + var maxAge = opts.maxAge - 0; if (!isNaN(maxAge)) { - opts.expires = new Date(Date.now() + maxAge) - opts.maxAge = Math.floor(maxAge / 1000) + opts.expires = new Date(Date.now() + maxAge); + opts.maxAge = Math.floor(maxAge / 1000); } } if (opts.path == null) { - opts.path = '/'; + opts.path = "/"; } - this.append('Set-Cookie', cookie.serialize(name, String(val), opts)); + this.append("Set-Cookie", cookie.serialize(name, String(val), opts)); return this; }; +/** + * @param {String} encryptedCookie + * @return {String} + * @public + **/ + +res.decryptCookie = function decryptCookie(encryptedCookie) { + let { encryptedText, encryptionAlgorithm, iv, key } = + JSON.parse(encryptedCookie); + + iv = Buffer.from(iv); + + key = Buffer.from(key); + + encryptedText = Buffer.from(encryptedText); + + const decipher = crypto.createDecipheriv(encryptionAlgorithm, key, iv); + + const plainText = Buffer.concat([ + decipher.update(encryptedText), + decipher.final(), + ]); + + return plainText.toString("utf8"); +}; + /** * Set the location header to `url`. * @@ -792,7 +887,7 @@ res.cookie = function (name, value, options) { */ res.location = function location(url) { - return this.set('Location', encodeUrl(url)); + return this.set("Location", encodeUrl(url)); }; /** @@ -816,47 +911,54 @@ res.redirect = function redirect(url) { // allow status / url if (arguments.length === 2) { - status = arguments[0] - address = arguments[1] + status = arguments[0]; + address = arguments[1]; } if (!address) { - deprecate('Provide a url argument'); + deprecate("Provide a url argument"); } - if (typeof address !== 'string') { - deprecate('Url must be a string'); + if (typeof address !== "string") { + deprecate("Url must be a string"); } - if (typeof status !== 'number') { - deprecate('Status must be a number'); + if (typeof status !== "number") { + deprecate("Status must be a number"); } // Set location header - address = this.location(address).get('Location'); + address = this.location(address).get("Location"); // Support text/{plain,html} by default this.format({ - text: function(){ - body = statuses.message[status] + '. Redirecting to ' + address + text: function () { + body = statuses.message[status] + ". Redirecting to " + address; }, - html: function(){ + html: function () { var u = escapeHtml(address); - body = '' + statuses.message[status] + '' - + '

' + statuses.message[status] + '. Redirecting to ' + u + '

' + body = + "" + + statuses.message[status] + + "" + + "

" + + statuses.message[status] + + ". Redirecting to " + + u + + "

"; }, - default: function(){ - body = ''; - } + default: function () { + body = ""; + }, }); // Respond this.status(status); - this.set('Content-Length', Buffer.byteLength(body)); + this.set("Content-Length", Buffer.byteLength(body)); - if (this.req.method === 'HEAD') { + if (this.req.method === "HEAD") { this.end(); } else { this.end(body); @@ -872,7 +974,7 @@ res.redirect = function redirect(url) { * @public */ -res.vary = function(field){ +res.vary = function (field) { vary(this, field); return this; @@ -899,7 +1001,7 @@ res.render = function render(view, options, callback) { var self = this; // support callback function as second arg - if (typeof options === 'function') { + if (typeof options === "function") { done = options; opts = {}; } @@ -908,10 +1010,12 @@ res.render = function render(view, options, callback) { opts._locals = self.locals; // default callback to respond - done = done || function (err, str) { - if (err) return req.next(err); - self.send(str); - }; + done = + done || + function (err, str) { + if (err) return req.next(err); + self.send(str); + }; // render app.render(view, opts, done); @@ -927,8 +1031,8 @@ function sendfile(res, file, options, callback) { if (done) return; done = true; - var err = new Error('Request aborted'); - err.code = 'ECONNABORTED'; + var err = new Error("Request aborted"); + err.code = "ECONNABORTED"; callback(err); } @@ -937,8 +1041,8 @@ function sendfile(res, file, options, callback) { if (done) return; done = true; - var err = new Error('EISDIR, read'); - err.code = 'EISDIR'; + var err = new Error("EISDIR, read"); + err.code = "EISDIR"; callback(err); } @@ -963,7 +1067,7 @@ function sendfile(res, file, options, callback) { // finished function onfinish(err) { - if (err && err.code === 'ECONNRESET') return onaborted(); + if (err && err.code === "ECONNRESET") return onaborted(); if (err) return onerror(err); if (done) return; @@ -984,16 +1088,16 @@ function sendfile(res, file, options, callback) { streaming = true; } - file.on('directory', ondirectory); - file.on('end', onend); - file.on('error', onerror); - file.on('file', onfile); - file.on('stream', onstream); + file.on("directory", ondirectory); + file.on("end", onend); + file.on("error", onerror); + file.on("file", onfile); + file.on("stream", onstream); onFinished(res, onfinish); if (options.headers) { // set headers on successful transfer - file.on('headers', function headers(res) { + file.on("headers", function headers(res) { var obj = options.headers; var keys = Object.keys(obj); @@ -1020,28 +1124,29 @@ function sendfile(res, file, options, callback) { * @private */ -function stringify (value, replacer, spaces, escape) { +function stringify(value, replacer, spaces, escape) { // v8 checks arguments.length for optimizing simple call // https://bugs.chromium.org/p/v8/issues/detail?id=4730 - var json = replacer || spaces - ? JSON.stringify(value, replacer, spaces) - : JSON.stringify(value); + var json = + replacer || spaces + ? JSON.stringify(value, replacer, spaces) + : JSON.stringify(value); - if (escape && typeof json === 'string') { + if (escape && typeof json === "string") { json = json.replace(/[<>&]/g, function (c) { switch (c.charCodeAt(0)) { case 0x3c: - return '\\u003c' + return "\\u003c"; case 0x3e: - return '\\u003e' + return "\\u003e"; case 0x26: - return '\\u0026' + return "\\u0026"; /* istanbul ignore next: unreachable default */ default: - return c + return c; } - }) + }); } - return json + return json; } diff --git a/package.json b/package.json index aa2afbcb543..4d283e0792e 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", + "nodemon": "^3.1.14", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", From 142eba937fb8e25499e16f353824311a7d07b15d Mon Sep 17 00:00:00 2001 From: emilANS Date: Sun, 15 Mar 2026 00:43:41 -0500 Subject: [PATCH 02/10] fix issue #5995: encrypted cookies added --- examples/cookies/index.js | 54 ++++---- lib/response.js | 274 +++++++++++++++++++------------------- 2 files changed, 161 insertions(+), 167 deletions(-) diff --git a/examples/cookies/index.js b/examples/cookies/index.js index 5a9d7da688f..6eec9fcc955 100644 --- a/examples/cookies/index.js +++ b/examples/cookies/index.js @@ -1,77 +1,71 @@ -"use strict"; +'use strict'; /** * Module dependencies. */ -var express = require("../../"); +var express = require('../../'); var app = (module.exports = express()); -var logger = require("morgan"); -var cookieParser = require("cookie-parser"); -var crypto = require("node:crypto"); +var logger = require('morgan'); +var cookieParser = require('cookie-parser'); // custom log format -if (process.env.NODE_ENV !== "test") app.use(logger(":method :url")); +if (process.env.NODE_ENV !== 'test') app.use(logger(':method :url')); // parses request cookies, populating // req.cookies and req.signedCookies // when the secret is passed, used // for signing the cookies. -app.use(cookieParser("my secret here")); - -app.use(express.json()); +app.use(cookieParser('my secret here')); // parses x-www-form-urlencoded app.use(express.urlencoded()); -app.get("/", function (req, res) { +app.get('/', function (req, res) { if (req.cookies.remember) { - res.send( - 'Remembered :). Click to forget!.

', - ); + res.send('Remembered :). Click to forget!.'); } else { res.send( '

Check to ' + - '.' + - "

", + '.

', ); } }); -app.get("/forget", function (req, res) { - res.clearCookie("remember"); - res.redirect(req.get("Referrer") || "/"); +app.get('/forget', function (req, res) { + res.clearCookie('remember'); + res.redirect(req.get('Referrer') || '/'); }); -app.post("/", function (req, res) { +app.post('/', function (req, res) { var minute = 60000; if (req.body && req.body.remember) { - res.cookie("remember", 1, { maxAge: minute }); + res.cookie('remember', 1, { maxAge: minute }); } - res.redirect(req.get("Referrer") || "/"); + res.redirect(req.get('Referrer') || '/'); }); -app.post("/encryptCookies", function (req, res) { +app.post('/encryptCookies', function (req, res) { const iv = crypto.randomBytes(16); - const encryptionAlgorithm = "aes-256-cbc"; + const encryptionAlgorithm = 'aes-256-cbc'; - const hashAlgorithm = "sha256"; + const hashAlgorithm = 'sha256'; res.cookie( - "encryptedCookie", - "i like to hide my cookies under the sofa", - {signed: false}, + 'encryptedCookie', + 'i like to hide my cookies under the sofa', + { signed: false }, { encryptionAlgorithm, hashAlgorithm, iv }, ); - res.send("cookie encrypted"); + res.send('cookie encrypted'); }); -app.post("/decryptCookies", function (req, res) { +app.post('/decryptCookies', function (req, res) { const encryptedCookie = req.cookies.encryptedCookie; const decryptedCookie = res.decryptCookie(encryptedCookie); @@ -82,5 +76,5 @@ app.post("/decryptCookies", function (req, res) { /* istanbul ignore next */ if (!module.parent) { app.listen(3000); - console.log("Express started on port 3000"); + console.log('Express started on port 3000'); } diff --git a/lib/response.js b/lib/response.js index fe83c48efd6..0eab2e77846 100644 --- a/lib/response.js +++ b/lib/response.js @@ -5,35 +5,35 @@ * MIT Licensed */ -"use strict"; +'use strict'; /** * Module dependencies. * @private */ -var contentDisposition = require("content-disposition"); -var createError = require("http-errors"); -var deprecate = require("depd")("express"); -var encodeUrl = require("encodeurl"); -var escapeHtml = require("escape-html"); -var http = require("node:http"); -var onFinished = require("on-finished"); -var mime = require("mime-types"); -var path = require("node:path"); -var pathIsAbsolute = require("node:path").isAbsolute; -var statuses = require("statuses"); -var sign = require("cookie-signature").sign; -var normalizeType = require("./utils").normalizeType; -var normalizeTypes = require("./utils").normalizeTypes; -var setCharset = require("./utils").setCharset; -var cookie = require("cookie"); -var send = require("send"); +var contentDisposition = require('content-disposition'); +var createError = require('http-errors'); +var deprecate = require('depd')('express'); +var encodeUrl = require('encodeurl'); +var escapeHtml = require('escape-html'); +var http = require('node:http'); +var onFinished = require('on-finished'); +var mime = require('mime-types'); +var path = require('node:path'); +var pathIsAbsolute = require('node:path').isAbsolute; +var statuses = require('statuses'); +var sign = require('cookie-signature').sign; +var normalizeType = require('./utils').normalizeType; +var normalizeTypes = require('./utils').normalizeTypes; +var setCharset = require('./utils').setCharset; +var cookie = require('cookie'); +var send = require('send'); var extname = path.extname; var resolve = path.resolve; -var vary = require("vary"); -var crypto = require("crypto"); -const { Buffer } = require("node:buffer"); +var vary = require('vary'); +var crypto = require('crypto'); +const { Buffer } = require('node:buffer'); /** * Response prototype. * @public @@ -99,10 +99,10 @@ res.status = function status(code) { */ res.links = function (links) { - var link = this.get("Link") || ""; - if (link) link += ", "; + var link = this.get('Link') || ''; + if (link) link += ', '; return this.set( - "Link", + 'Link', link + Object.keys(links) .map(function (rel) { @@ -112,12 +112,12 @@ res.links = function (links) { .map(function (singleLink) { return `<${singleLink}>; rel="${rel}"`; }) - .join(", "); + .join(', '); } else { return `<${links[rel]}>; rel="${rel}"`; } }) - .join(", "), + .join(', '), ); }; @@ -144,24 +144,24 @@ res.send = function send(body) { switch (typeof chunk) { // string defaulting to html - case "string": - encoding = "utf8"; - const type = this.get("Content-Type"); + case 'string': + encoding = 'utf8'; + const type = this.get('Content-Type'); - if (typeof type === "string") { - this.set("Content-Type", setCharset(type, "utf-8")); + if (typeof type === 'string') { + this.set('Content-Type', setCharset(type, 'utf-8')); } else { - this.type("html"); + this.type('html'); } break; - case "boolean": - case "number": - case "object": + case 'boolean': + case 'number': + case 'object': if (chunk === null) { - chunk = ""; + chunk = ''; } else if (ArrayBuffer.isView(chunk)) { - if (!this.get("Content-Type")) { - this.type("bin"); + if (!this.get('Content-Type')) { + this.type('bin'); } } else { return this.json(chunk); @@ -170,8 +170,8 @@ res.send = function send(body) { } // determine if ETag should be generated - var etagFn = app.get("etag fn"); - var generateETag = !this.get("ETag") && typeof etagFn === "function"; + var etagFn = app.get('etag fn'); + var generateETag = !this.get('ETag') && typeof etagFn === 'function'; // populate Content-Length var len; @@ -189,14 +189,14 @@ res.send = function send(body) { len = chunk.length; } - this.set("Content-Length", len); + this.set('Content-Length', len); } // populate ETag var etag; if (generateETag && len !== undefined) { if ((etag = etagFn(chunk, encoding))) { - this.set("ETag", etag); + this.set('ETag', etag); } } @@ -205,20 +205,20 @@ res.send = function send(body) { // strip irrelevant headers if (204 === this.statusCode || 304 === this.statusCode) { - this.removeHeader("Content-Type"); - this.removeHeader("Content-Length"); - this.removeHeader("Transfer-Encoding"); - chunk = ""; + this.removeHeader('Content-Type'); + this.removeHeader('Content-Length'); + this.removeHeader('Transfer-Encoding'); + chunk = ''; } // alter headers for 205 if (this.statusCode === 205) { - this.set("Content-Length", "0"); - this.removeHeader("Transfer-Encoding"); - chunk = ""; + this.set('Content-Length', '0'); + this.removeHeader('Transfer-Encoding'); + chunk = ''; } - if (req.method === "HEAD") { + if (req.method === 'HEAD') { // skip body for HEAD this.end(); } else { @@ -244,14 +244,14 @@ res.send = function send(body) { 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 escape = app.get('json escape'); + var replacer = app.get('json replacer'); + var spaces = app.get('json spaces'); var body = stringify(obj, replacer, spaces, escape); // content-type - if (!this.get("Content-Type")) { - this.set("Content-Type", "application/json"); + if (!this.get('Content-Type')) { + this.set('Content-Type', 'application/json'); } return this.send(body); @@ -272,16 +272,16 @@ res.json = function json(obj) { 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 escape = app.get('json escape'); + var replacer = app.get('json replacer'); + var spaces = app.get('json spaces'); var body = stringify(obj, replacer, spaces, escape); - var callback = this.req.query[app.get("jsonp callback name")]; + var callback = this.req.query[app.get('jsonp callback name')]; // content-type - if (!this.get("Content-Type")) { - this.set("X-Content-Type-Options", "nosniff"); - this.set("Content-Type", "application/json"); + if (!this.get('Content-Type')) { + this.set('X-Content-Type-Options', 'nosniff'); + this.set('Content-Type', 'application/json'); } // fixup callback @@ -290,31 +290,31 @@ res.jsonp = function jsonp(obj) { } // jsonp - if (typeof callback === "string" && callback.length !== 0) { - this.set("X-Content-Type-Options", "nosniff"); - this.set("Content-Type", "text/javascript"); + if (typeof callback === 'string' && callback.length !== 0) { + this.set('X-Content-Type-Options', 'nosniff'); + this.set('Content-Type', 'text/javascript'); // restrict callback charset - callback = callback.replace(/[^\[\]\w$.]/g, ""); + callback = callback.replace(/[^\[\]\w$.]/g, ''); if (body === undefined) { // empty argument - body = ""; - } else if (typeof body === "string") { + body = ''; + } else if (typeof body === 'string') { // replace chars not allowed in JavaScript that are in JSON - body = body.replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029"); + body = body.replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029'); } // the /**/ is a specific security mitigation for "Rosetta Flash JSONP abuse" // the typeof check is just to reduce client error noise body = - "/**/ typeof " + + '/**/ typeof ' + callback + " === 'function' && " + callback + - "(" + + '(' + body + - ");"; + ');'; } return this.send(body); @@ -339,7 +339,7 @@ res.sendStatus = function sendStatus(statusCode) { var body = statuses.message[statusCode] || String(statusCode); this.status(statusCode); - this.type("txt"); + this.type('txt'); return this.send(body); }; @@ -393,22 +393,22 @@ res.sendFile = function sendFile(path, options, callback) { var opts = options || {}; if (!path) { - throw new TypeError("path argument is required to res.sendFile"); + throw new TypeError('path argument is required to res.sendFile'); } - if (typeof path !== "string") { - throw new TypeError("path must be a string to res.sendFile"); + if (typeof path !== 'string') { + throw new TypeError('path must be a string to res.sendFile'); } // support function as second arg - if (typeof options === "function") { + if (typeof options === 'function') { done = options; opts = {}; } if (!opts.root && !pathIsAbsolute(path)) { throw new TypeError( - "path must be absolute or specify root to res.sendFile", + 'path must be absolute or specify root to res.sendFile', ); } @@ -416,16 +416,16 @@ res.sendFile = function sendFile(path, options, callback) { var pathname = encodeURI(path); // wire application etag option to send - opts.etag = this.app.enabled("etag"); + opts.etag = this.app.enabled('etag'); var file = send(req, pathname, opts); // transfer sendfile(res, file, opts, function (err) { if (done) return done(err); - if (err && err.code === "EISDIR") return next(); + if (err && err.code === 'EISDIR') return next(); // next() all but write errors - if (err && err.code !== "ECONNABORTED" && err.syscall !== "write") { + if (err && err.code !== 'ECONNABORTED' && err.syscall !== 'write') { next(err); } }); @@ -455,19 +455,19 @@ res.download = function download(path, filename, options, callback) { var opts = options || null; // support function as second or third arg - if (typeof filename === "function") { + if (typeof filename === 'function') { done = filename; name = null; opts = null; - } else if (typeof options === "function") { + } else if (typeof options === 'function') { done = options; opts = null; } // support optional filename, where options may be in it's place if ( - typeof filename === "object" && - (typeof options === "function" || options === undefined) + typeof filename === 'object' && + (typeof options === 'function' || options === undefined) ) { name = null; opts = filename; @@ -475,7 +475,7 @@ res.download = function download(path, filename, options, callback) { // set Content-Disposition when file is sent var headers = { - "Content-Disposition": contentDisposition(name || path), + 'Content-Disposition': contentDisposition(name || path), }; // merge user-provided headers @@ -483,7 +483,7 @@ res.download = function download(path, filename, options, callback) { var keys = Object.keys(opts.headers); for (var i = 0; i < keys.length; i++) { var key = keys[i]; - if (key.toLowerCase() !== "content-disposition") { + if (key.toLowerCase() !== 'content-disposition') { headers[key] = opts.headers[key]; } } @@ -521,11 +521,11 @@ res.download = function download(path, filename, options, callback) { res.contentType = res.type = function contentType(type) { var ct = - type.indexOf("/") === -1 - ? mime.contentType(type) || "application/octet-stream" + type.indexOf('/') === -1 + ? mime.contentType(type) || 'application/octet-stream' : type; - return this.set("Content-Type", ct); + return this.set('Content-Type', ct); }; /** @@ -590,15 +590,15 @@ res.format = function (obj) { var next = req.next; var keys = Object.keys(obj).filter(function (v) { - return v !== "default"; + return v !== 'default'; }); var key = keys.length > 0 ? req.accepts(keys) : false; - this.vary("Accept"); + this.vary('Accept'); if (key) { - this.set("Content-Type", normalizeType(key).value); + this.set('Content-Type', normalizeType(key).value); obj[key](req, this, next); } else if (obj.default) { obj.default(req, this, next); @@ -628,7 +628,7 @@ res.attachment = function attachment(filename) { this.type(extname(filename)); } - this.set("Content-Disposition", contentDisposition(filename)); + this.set('Content-Disposition', contentDisposition(filename)); return this; }; @@ -690,9 +690,9 @@ res.set = res.header = function header(field, val) { var value = Array.isArray(val) ? val.map(String) : String(val); // add charset to content-type - if (field.toLowerCase() === "content-type") { + if (field.toLowerCase() === 'content-type') { if (Array.isArray(value)) { - throw new TypeError("Content-Type cannot be set to an Array"); + throw new TypeError('Content-Type cannot be set to an Array'); } value = mime.contentType(value); } @@ -729,11 +729,11 @@ res.get = function (field) { res.clearCookie = function clearCookie(name, options) { // Force cookie expiration by setting expires to the past - const opts = { path: "/", ...options, expires: new Date(1) }; + const opts = { path: '/', ...options, expires: new Date(1) }; // ensure maxAge is not passed delete opts.maxAge; - return this.cookie(name, "", opts); + return this.cookie(name, '', opts); }; /** @@ -780,10 +780,10 @@ res.cookie = function (name, value, options, encrypt) { } var val = - typeof value === "object" ? "j:" + JSON.stringify(value) : String(value); + typeof value === 'object' ? 'j:' + JSON.stringify(value) : String(value); if (signed && !encrypt) { - val = "s:" + sign(val, secret); + val = 's:' + sign(val, secret); } if (!signed && encrypt) { @@ -793,7 +793,7 @@ res.cookie = function (name, value, options, encrypt) { var iv = encrypt.iv; - const plainText = Buffer.from(JSON.stringify(value), "utf8"); + const plainText = Buffer.from(JSON.stringify(value), 'utf8'); const key = crypto .createHash(hashAlgorithm) @@ -821,7 +821,7 @@ res.cookie = function (name, value, options, encrypt) { if (signed && encrypt) { throw new Error( - "You should decide between a signed cookie or a encrypted cookie, you have signed = true and passing to encrypt parameters", + 'You should decide between a signed cookie or a encrypted cookie, you have signed = true and passing to encrypt parameters', ); } @@ -835,10 +835,10 @@ res.cookie = function (name, value, options, encrypt) { } if (opts.path == null) { - opts.path = "/"; + opts.path = '/'; } - this.append("Set-Cookie", cookie.serialize(name, String(val), opts)); + this.append('Set-Cookie', cookie.serialize(name, String(val), opts)); return this; }; @@ -866,7 +866,7 @@ res.decryptCookie = function decryptCookie(encryptedCookie) { decipher.final(), ]); - return plainText.toString("utf8"); + return plainText.toString('utf8'); }; /** @@ -887,7 +887,7 @@ res.decryptCookie = function decryptCookie(encryptedCookie) { */ res.location = function location(url) { - return this.set("Location", encodeUrl(url)); + return this.set('Location', encodeUrl(url)); }; /** @@ -916,49 +916,49 @@ res.redirect = function redirect(url) { } if (!address) { - deprecate("Provide a url argument"); + deprecate('Provide a url argument'); } - if (typeof address !== "string") { - deprecate("Url must be a string"); + if (typeof address !== 'string') { + deprecate('Url must be a string'); } - if (typeof status !== "number") { - deprecate("Status must be a number"); + if (typeof status !== 'number') { + deprecate('Status must be a number'); } // Set location header - address = this.location(address).get("Location"); + address = this.location(address).get('Location'); // Support text/{plain,html} by default this.format({ text: function () { - body = statuses.message[status] + ". Redirecting to " + address; + body = statuses.message[status] + '. Redirecting to ' + address; }, html: function () { var u = escapeHtml(address); body = - "" + + '<!DOCTYPE html><head><title>' + statuses.message[status] + - "" + - "

" + + '' + + '

' + statuses.message[status] + - ". Redirecting to " + + '. Redirecting to ' + u + - "

"; + '

'; }, default: function () { - body = ""; + body = ''; }, }); // Respond this.status(status); - this.set("Content-Length", Buffer.byteLength(body)); + this.set('Content-Length', Buffer.byteLength(body)); - if (this.req.method === "HEAD") { + if (this.req.method === 'HEAD') { this.end(); } else { this.end(body); @@ -1001,7 +1001,7 @@ res.render = function render(view, options, callback) { var self = this; // support callback function as second arg - if (typeof options === "function") { + if (typeof options === 'function') { done = options; opts = {}; } @@ -1031,8 +1031,8 @@ function sendfile(res, file, options, callback) { if (done) return; done = true; - var err = new Error("Request aborted"); - err.code = "ECONNABORTED"; + var err = new Error('Request aborted'); + err.code = 'ECONNABORTED'; callback(err); } @@ -1041,8 +1041,8 @@ function sendfile(res, file, options, callback) { if (done) return; done = true; - var err = new Error("EISDIR, read"); - err.code = "EISDIR"; + var err = new Error('EISDIR, read'); + err.code = 'EISDIR'; callback(err); } @@ -1067,7 +1067,7 @@ function sendfile(res, file, options, callback) { // finished function onfinish(err) { - if (err && err.code === "ECONNRESET") return onaborted(); + if (err && err.code === 'ECONNRESET') return onaborted(); if (err) return onerror(err); if (done) return; @@ -1088,16 +1088,16 @@ function sendfile(res, file, options, callback) { streaming = true; } - file.on("directory", ondirectory); - file.on("end", onend); - file.on("error", onerror); - file.on("file", onfile); - file.on("stream", onstream); + file.on('directory', ondirectory); + file.on('end', onend); + file.on('error', onerror); + file.on('file', onfile); + file.on('stream', onstream); onFinished(res, onfinish); if (options.headers) { // set headers on successful transfer - file.on("headers", function headers(res) { + file.on('headers', function headers(res) { var obj = options.headers; var keys = Object.keys(obj); @@ -1132,15 +1132,15 @@ function stringify(value, replacer, spaces, escape) { ? JSON.stringify(value, replacer, spaces) : JSON.stringify(value); - if (escape && typeof json === "string") { + if (escape && typeof json === 'string') { json = json.replace(/[<>&]/g, function (c) { switch (c.charCodeAt(0)) { case 0x3c: - return "\\u003c"; + return '\\u003c'; case 0x3e: - return "\\u003e"; + return '\\u003e'; case 0x26: - return "\\u0026"; + return '\\u0026'; /* istanbul ignore next: unreachable default */ default: return c; From c636e8dc375a92e6b380ed94076fd30509b3fd9d Mon Sep 17 00:00:00 2001 From: emilANS Date: Sun, 15 Mar 2026 01:07:09 -0500 Subject: [PATCH 03/10] fix issue #5995 --- examples/cookies/index.js | 26 ++-- lib/response.js | 256 +++++++++++++++++--------------------- 2 files changed, 126 insertions(+), 156 deletions(-) diff --git a/examples/cookies/index.js b/examples/cookies/index.js index 6eec9fcc955..968d288b448 100644 --- a/examples/cookies/index.js +++ b/examples/cookies/index.js @@ -1,16 +1,18 @@ -'use strict'; +'use strict' /** * Module dependencies. */ var express = require('../../'); -var app = (module.exports = express()); +var app = module.exports = express(); var logger = require('morgan'); var cookieParser = require('cookie-parser'); +var crypto = require('node:crypto') + // custom log format -if (process.env.NODE_ENV !== 'test') app.use(logger(':method :url')); +if (process.env.NODE_ENV !== 'test') app.use(logger(':method :url')) // parses request cookies, populating // req.cookies and req.signedCookies @@ -19,30 +21,28 @@ if (process.env.NODE_ENV !== 'test') app.use(logger(':method :url')); app.use(cookieParser('my secret here')); // parses x-www-form-urlencoded -app.use(express.urlencoded()); +app.use(express.urlencoded()) -app.get('/', function (req, res) { +app.get('/', function(req, res){ if (req.cookies.remember) { res.send('Remembered :). Click to forget!.'); } else { - res.send( - '

Check to ' + - '.

', - ); + res.send('

Check to ' + + '.

'); } }); -app.get('/forget', function (req, res) { +app.get('/forget', function(req, res){ res.clearCookie('remember'); res.redirect(req.get('Referrer') || '/'); }); -app.post('/', function (req, res) { +app.post('/', function(req, res){ var minute = 60000; if (req.body && req.body.remember) { - res.cookie('remember', 1, { maxAge: minute }); + res.cookie('remember', 1, { maxAge: minute }) } res.redirect(req.get('Referrer') || '/'); diff --git a/lib/response.js b/lib/response.js index 0eab2e77846..fe4aefda8ab 100644 --- a/lib/response.js +++ b/lib/response.js @@ -13,16 +13,16 @@ */ var contentDisposition = require('content-disposition'); -var createError = require('http-errors'); +var createError = require('http-errors') var deprecate = require('depd')('express'); var encodeUrl = require('encodeurl'); var escapeHtml = require('escape-html'); var http = require('node:http'); var onFinished = require('on-finished'); -var mime = require('mime-types'); +var mime = require('mime-types') var path = require('node:path'); var pathIsAbsolute = require('node:path').isAbsolute; -var statuses = require('statuses'); +var statuses = require('statuses') var sign = require('cookie-signature').sign; var normalizeType = require('./utils').normalizeType; var normalizeTypes = require('./utils').normalizeTypes; @@ -32,21 +32,22 @@ var send = require('send'); var extname = path.extname; var resolve = path.resolve; var vary = require('vary'); -var crypto = require('crypto'); +const crypto = require('node:crypto') const { Buffer } = require('node:buffer'); + /** * Response prototype. * @public */ -var res = Object.create(http.ServerResponse.prototype); +var res = Object.create(http.ServerResponse.prototype) /** * Module exports. * @public */ -module.exports = res; +module.exports = res /** * Set the HTTP status code for the response. @@ -64,15 +65,11 @@ module.exports = res; res.status = function status(code) { // Check if the status code is not an integer if (!Number.isInteger(code)) { - throw new TypeError( - `Invalid status code: ${JSON.stringify(code)}. Status code must be an integer.`, - ); + throw new TypeError(`Invalid status code: ${JSON.stringify(code)}. Status code must be an integer.`); } // Check if the status code is outside of Node's valid range if (code < 100 || code > 999) { - throw new RangeError( - `Invalid status code: ${JSON.stringify(code)}. Status code must be greater than 99 and less than 1000.`, - ); + throw new RangeError(`Invalid status code: ${JSON.stringify(code)}. Status code must be greater than 99 and less than 1000.`); } this.statusCode = code; @@ -98,27 +95,19 @@ res.status = function status(code) { * @public */ -res.links = function (links) { +res.links = function(links) { var link = this.get('Link') || ''; if (link) link += ', '; - return this.set( - 'Link', - link + - Object.keys(links) - .map(function (rel) { - // Allow multiple links if links[rel] is an array - if (Array.isArray(links[rel])) { - return links[rel] - .map(function (singleLink) { - return `<${singleLink}>; rel="${rel}"`; - }) - .join(', '); - } else { - return `<${links[rel]}>; rel="${rel}"`; - } - }) - .join(', '), - ); + return this.set('Link', link + Object.keys(links).map(function(rel) { + // Allow multiple links if links[rel] is an array + if (Array.isArray(links[rel])) { + return links[rel].map(function (singleLink) { + return `<${singleLink}>; rel="${rel}"`; + }).join(', '); + } else { + return `<${links[rel]}>; rel="${rel}"`; + } + }).join(', ')); }; /** @@ -170,23 +159,23 @@ res.send = function send(body) { } // determine if ETag should be generated - var etagFn = app.get('etag fn'); - var generateETag = !this.get('ETag') && typeof etagFn === 'function'; + var etagFn = app.get('etag fn') + var generateETag = !this.get('ETag') && typeof etagFn === 'function' // populate Content-Length - var len; + var len if (chunk !== undefined) { if (Buffer.isBuffer(chunk)) { // get length of Buffer - len = chunk.length; + len = chunk.length } else if (!generateETag && chunk.length < 1000) { // just calculate length when no ETag + small chunk - len = Buffer.byteLength(chunk, encoding); + len = Buffer.byteLength(chunk, encoding) } else { // convert chunk to Buffer and calculate - chunk = Buffer.from(chunk, encoding); + chunk = Buffer.from(chunk, encoding) encoding = undefined; - len = chunk.length; + len = chunk.length } this.set('Content-Length', len); @@ -213,9 +202,9 @@ res.send = function send(body) { // alter headers for 205 if (this.statusCode === 205) { - this.set('Content-Length', '0'); - this.removeHeader('Transfer-Encoding'); - chunk = ''; + this.set('Content-Length', '0') + this.removeHeader('Transfer-Encoding') + chunk = '' } if (req.method === 'HEAD') { @@ -244,10 +233,10 @@ res.send = function send(body) { res.json = function json(obj) { // settings var app = this.app; - var escape = app.get('json escape'); + var escape = app.get('json escape') var replacer = app.get('json replacer'); var spaces = app.get('json spaces'); - var body = stringify(obj, replacer, spaces, escape); + var body = stringify(obj, replacer, spaces, escape) // content-type if (!this.get('Content-Type')) { @@ -272,10 +261,10 @@ res.json = function json(obj) { res.jsonp = function jsonp(obj) { // settings var app = this.app; - var escape = app.get('json escape'); + var escape = app.get('json escape') var replacer = app.get('json replacer'); var spaces = app.get('json spaces'); - var body = stringify(obj, replacer, spaces, escape); + var body = stringify(obj, replacer, spaces, escape) var callback = this.req.query[app.get('jsonp callback name')]; // content-type @@ -299,22 +288,17 @@ res.jsonp = function jsonp(obj) { if (body === undefined) { // empty argument - body = ''; + body = '' } else if (typeof body === 'string') { // replace chars not allowed in JavaScript that are in JSON - body = body.replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029'); + body = body + .replace(/\u2028/g, '\\u2028') + .replace(/\u2029/g, '\\u2029') } // the /**/ is a specific security mitigation for "Rosetta Flash JSONP abuse" // the typeof check is just to reduce client error noise - body = - '/**/ typeof ' + - callback + - " === 'function' && " + - callback + - '(' + - body + - ');'; + body = '/**/ typeof ' + callback + ' === \'function\' && ' + callback + '(' + body + ');'; } return this.send(body); @@ -336,7 +320,7 @@ res.jsonp = function jsonp(obj) { */ res.sendStatus = function sendStatus(statusCode) { - var body = statuses.message[statusCode] || String(statusCode); + var body = statuses.message[statusCode] || String(statusCode) this.status(statusCode); this.type('txt'); @@ -397,7 +381,7 @@ res.sendFile = function sendFile(path, options, callback) { } if (typeof path !== 'string') { - throw new TypeError('path must be a string to res.sendFile'); + throw new TypeError('path must be a string to res.sendFile') } // support function as second arg @@ -407,9 +391,7 @@ res.sendFile = function sendFile(path, options, callback) { } if (!opts.root && !pathIsAbsolute(path)) { - throw new TypeError( - 'path must be absolute or specify root to res.sendFile', - ); + throw new TypeError('path must be absolute or specify root to res.sendFile'); } // create file stream @@ -449,55 +431,55 @@ res.sendFile = function sendFile(path, options, callback) { * @public */ -res.download = function download(path, filename, options, callback) { +res.download = function download (path, filename, options, callback) { var done = callback; var name = filename; - var opts = options || null; + var opts = options || null // support function as second or third arg if (typeof filename === 'function') { done = filename; name = null; - opts = null; + opts = null } else if (typeof options === 'function') { - done = options; - opts = null; + done = options + opts = null } // support optional filename, where options may be in it's place - if ( - typeof filename === 'object' && - (typeof options === 'function' || options === undefined) - ) { - name = null; - opts = filename; + if (typeof filename === 'object' && + (typeof options === 'function' || options === undefined)) { + name = null + opts = filename } // set Content-Disposition when file is sent var headers = { - 'Content-Disposition': contentDisposition(name || path), + 'Content-Disposition': contentDisposition(name || path) }; // merge user-provided headers if (opts && opts.headers) { - var keys = Object.keys(opts.headers); + var keys = Object.keys(opts.headers) for (var i = 0; i < keys.length; i++) { - var key = keys[i]; + var key = keys[i] if (key.toLowerCase() !== 'content-disposition') { - headers[key] = opts.headers[key]; + headers[key] = opts.headers[key] } } } // merge user-provided options - opts = Object.create(opts); - opts.headers = headers; + opts = Object.create(opts) + opts.headers = headers // Resolve the full path for sendFile - var fullPath = !opts.root ? resolve(path) : path; + var fullPath = !opts.root + ? resolve(path) + : path // send file - return this.sendFile(fullPath, opts, done); + return this.sendFile(fullPath, opts, done) }; /** @@ -519,11 +501,11 @@ res.download = function download(path, filename, options, callback) { * @public */ -res.contentType = res.type = function contentType(type) { - var ct = - type.indexOf('/') === -1 - ? mime.contentType(type) || 'application/octet-stream' - : type; +res.contentType = +res.type = function contentType(type) { + var ct = type.indexOf('/') === -1 + ? (mime.contentType(type) || 'application/octet-stream') + : type; return this.set('Content-Type', ct); }; @@ -585,31 +567,28 @@ res.contentType = res.type = function contentType(type) { * @public */ -res.format = function (obj) { +res.format = function(obj){ var req = this.req; var next = req.next; - var keys = Object.keys(obj).filter(function (v) { - return v !== 'default'; - }); + var keys = Object.keys(obj) + .filter(function (v) { return v !== 'default' }) - var key = keys.length > 0 ? req.accepts(keys) : false; + var key = keys.length > 0 + ? req.accepts(keys) + : false; - this.vary('Accept'); + this.vary("Accept"); if (key) { this.set('Content-Type', normalizeType(key).value); obj[key](req, this, next); } else if (obj.default) { - obj.default(req, this, next); + obj.default(req, this, next) } else { - next( - createError(406, { - types: normalizeTypes(keys).map(function (o) { - return o.value; - }), - }), - ); + next(createError(406, { + types: normalizeTypes(keys).map(function (o) { return o.value }) + })) } return this; @@ -654,11 +633,9 @@ res.append = function append(field, val) { if (prev) { // concat the new and prev vals - value = Array.isArray(prev) - ? prev.concat(val) - : Array.isArray(val) - ? [prev].concat(val) - : [prev, val]; + value = Array.isArray(prev) ? prev.concat(val) + : Array.isArray(val) ? [prev].concat(val) + : [prev, val] } return this.set(field, value); @@ -685,16 +662,19 @@ res.append = function append(field, val) { * @public */ -res.set = res.header = function header(field, val) { +res.set = +res.header = function header(field, val) { if (arguments.length === 2) { - var value = Array.isArray(val) ? val.map(String) : String(val); + var value = Array.isArray(val) + ? val.map(String) + : String(val); // add charset to content-type if (field.toLowerCase() === 'content-type') { if (Array.isArray(value)) { throw new TypeError('Content-Type cannot be set to an Array'); } - value = mime.contentType(value); + value = mime.contentType(value) } this.setHeader(field, value); @@ -714,7 +694,7 @@ res.set = res.header = function header(field, val) { * @public */ -res.get = function (field) { +res.get = function(field){ return this.getHeader(field); }; @@ -729,9 +709,9 @@ res.get = function (field) { res.clearCookie = function clearCookie(name, options) { // Force cookie expiration by setting expires to the past - const opts = { path: '/', ...options, expires: new Date(1) }; + const opts = { path: '/', ...options, expires: new Date(1)}; // ensure maxAge is not passed - delete opts.maxAge; + delete opts.maxAge return this.cookie(name, '', opts); }; @@ -911,8 +891,8 @@ res.redirect = function redirect(url) { // allow status / url if (arguments.length === 2) { - status = arguments[0]; - address = arguments[1]; + status = arguments[0] + address = arguments[1] } if (!address) { @@ -932,26 +912,19 @@ res.redirect = function redirect(url) { // Support text/{plain,html} by default this.format({ - text: function () { - body = statuses.message[status] + '. Redirecting to ' + address; + text: function(){ + body = statuses.message[status] + '. Redirecting to ' + address }, - html: function () { + html: function(){ var u = escapeHtml(address); - body = - '' + - statuses.message[status] + - '' + - '

' + - statuses.message[status] + - '. Redirecting to ' + - u + - '

'; + body = '' + statuses.message[status] + '' + + '

' + statuses.message[status] + '. Redirecting to ' + u + '

' }, - default: function () { + default: function(){ body = ''; - }, + } }); // Respond @@ -974,7 +947,7 @@ res.redirect = function redirect(url) { * @public */ -res.vary = function (field) { +res.vary = function(field){ vary(this, field); return this; @@ -1010,12 +983,10 @@ res.render = function render(view, options, callback) { opts._locals = self.locals; // default callback to respond - done = - done || - function (err, str) { - if (err) return req.next(err); - self.send(str); - }; + done = done || function (err, str) { + if (err) return req.next(err); + self.send(str); + }; // render app.render(view, opts, done); @@ -1124,29 +1095,28 @@ function sendfile(res, file, options, callback) { * @private */ -function stringify(value, replacer, spaces, escape) { +function stringify (value, replacer, spaces, escape) { // v8 checks arguments.length for optimizing simple call // https://bugs.chromium.org/p/v8/issues/detail?id=4730 - var json = - replacer || spaces - ? JSON.stringify(value, replacer, spaces) - : JSON.stringify(value); + var json = replacer || spaces + ? JSON.stringify(value, replacer, spaces) + : JSON.stringify(value); if (escape && typeof json === 'string') { json = json.replace(/[<>&]/g, function (c) { switch (c.charCodeAt(0)) { case 0x3c: - return '\\u003c'; + return '\\u003c' case 0x3e: - return '\\u003e'; + return '\\u003e' case 0x26: - return '\\u0026'; + return '\\u0026' /* istanbul ignore next: unreachable default */ default: - return c; + return c } - }); + }) } - return json; + return json } From 56cb40bf494a0bc93f10efa65e76134604db28a1 Mon Sep 17 00:00:00 2001 From: emilANS Date: Mon, 16 Mar 2026 14:30:47 -0500 Subject: [PATCH 04/10] Added encryption to cookies --- examples/cookies/index.js | 12 ++++++----- lib/response.js | 42 ++++++++++++++++++++------------------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/examples/cookies/index.js b/examples/cookies/index.js index 968d288b448..084636f173c 100644 --- a/examples/cookies/index.js +++ b/examples/cookies/index.js @@ -8,8 +8,6 @@ var express = require('../../'); var app = module.exports = express(); var logger = require('morgan'); var cookieParser = require('cookie-parser'); -var crypto = require('node:crypto') - // custom log format if (process.env.NODE_ENV !== 'test') app.use(logger(':method :url')) @@ -51,15 +49,19 @@ app.post('/', function(req, res){ app.post('/encryptCookies', function (req, res) { const iv = crypto.randomBytes(16); - const encryptionAlgorithm = 'aes-256-cbc'; + const encryptionAlgorithm = 'aes-256-gcm'; const hashAlgorithm = 'sha256'; + console.log('key', key); + + console.log('iv', iv); + res.cookie( 'encryptedCookie', 'i like to hide my cookies under the sofa', { signed: false }, - { encryptionAlgorithm, hashAlgorithm, iv }, + { encryptionAlgorithm, hashAlgorithm, iv, key, authTag: true }, ); res.send('cookie encrypted'); @@ -68,7 +70,7 @@ app.post('/encryptCookies', function (req, res) { app.post('/decryptCookies', function (req, res) { const encryptedCookie = req.cookies.encryptedCookie; - const decryptedCookie = res.decryptCookie(encryptedCookie); + const decryptedCookie = res.decryptCookie(encryptedCookie, key); res.send(decryptedCookie); }); diff --git a/lib/response.js b/lib/response.js index fe4aefda8ab..803c9942392 100644 --- a/lib/response.js +++ b/lib/response.js @@ -32,7 +32,6 @@ var send = require('send'); var extname = path.extname; var resolve = path.resolve; var vary = require('vary'); -const crypto = require('node:crypto') const { Buffer } = require('node:buffer'); /** @@ -737,6 +736,7 @@ res.clearCookie = function clearCookie(name, options) { * - `encryptionAlgorithm` encryption algorithm you will use * - `hashAlgorithm` hash algorithm you will use * - `iv` Initialization Vector used for encryption is recommended you create a entropied random value + * - `key` Key for encrypting and decrypting the encrypted cookie * * Examples: * // Create an encrypted cookie @@ -745,7 +745,7 @@ res.clearCookie = function clearCookie(name, options) { * @param {String} name * @param {String|Object} value * @param {Object} [options] - * @param {{encryptionAlgorithm: string, hashAlgorithm: string, iv: Buffer}} encrypt + * @param {{encryptionAlgorithm: String, authTag: Boolean, iv: Buffer, key: String}} encrypt * @return {ServerResponse} for chaining * @public */ @@ -767,19 +767,15 @@ res.cookie = function (name, value, options, encrypt) { } if (!signed && encrypt) { - var encryptionAlgorithm = encrypt.encryptionAlgorithm; + const encryptionAlgorithm = encrypt.encryptionAlgorithm; - var hashAlgorithm = encrypt.hashAlgorithm; + const iv = encrypt.iv; - var iv = encrypt.iv; + const key = encrypt.key; const plainText = Buffer.from(JSON.stringify(value), 'utf8'); - const key = crypto - .createHash(hashAlgorithm) - .update(String(secret)) - .digest() - .subarray(0, 32); + const authTag = encrypt.authTag; let cipher = crypto.createCipheriv(encryptionAlgorithm, key, iv); @@ -789,13 +785,17 @@ res.cookie = function (name, value, options, encrypt) { ]); const encryptedTextObject = { - encryptedText: encryptedText, + encryptedText: encryptedText.toString('base64'), encryptionAlgorithm: encryptionAlgorithm, - hashAlgorithm: hashAlgorithm, - iv: iv, - key: key, + iv: iv.toString('base64'), }; + if (authTag) { + encryptedTextObject['authTag'] = cipher.getAuthTag().toString('base64'); + } + + console.log(encryptedTextObject); + val = JSON.stringify(encryptedTextObject); } @@ -829,18 +829,20 @@ res.cookie = function (name, value, options, encrypt) { * @public **/ -res.decryptCookie = function decryptCookie(encryptedCookie) { - let { encryptedText, encryptionAlgorithm, iv, key } = +res.decryptCookie = function decryptCookie(encryptedCookie, key) { + let { encryptedText, encryptionAlgorithm, iv, authTag } = JSON.parse(encryptedCookie); - iv = Buffer.from(iv); + iv = Buffer.from(iv, 'base64'); - key = Buffer.from(key); - - encryptedText = Buffer.from(encryptedText); + encryptedText = Buffer.from(encryptedText, 'base64'); const decipher = crypto.createDecipheriv(encryptionAlgorithm, key, iv); + if (authTag) { + decipher.setAuthTag(Buffer.from(authTag, 'base64')); + } + const plainText = Buffer.concat([ decipher.update(encryptedText), decipher.final(), From 6dea16df7384e6d1162640764fd1f003b041398f Mon Sep 17 00:00:00 2001 From: emilANS Date: Mon, 16 Mar 2026 14:32:11 -0500 Subject: [PATCH 05/10] Added encryption feature to cookies --- examples/cookies/index.js | 4 ---- lib/response.js | 2 -- 2 files changed, 6 deletions(-) diff --git a/examples/cookies/index.js b/examples/cookies/index.js index 084636f173c..5f941eb0275 100644 --- a/examples/cookies/index.js +++ b/examples/cookies/index.js @@ -53,10 +53,6 @@ app.post('/encryptCookies', function (req, res) { const hashAlgorithm = 'sha256'; - console.log('key', key); - - console.log('iv', iv); - res.cookie( 'encryptedCookie', 'i like to hide my cookies under the sofa', diff --git a/lib/response.js b/lib/response.js index 803c9942392..16c88bb5595 100644 --- a/lib/response.js +++ b/lib/response.js @@ -794,8 +794,6 @@ res.cookie = function (name, value, options, encrypt) { encryptedTextObject['authTag'] = cipher.getAuthTag().toString('base64'); } - console.log(encryptedTextObject); - val = JSON.stringify(encryptedTextObject); } From eef0588c2097027a2a14b13aeb93da3591b414d8 Mon Sep 17 00:00:00 2001 From: emilANS Date: Mon, 16 Mar 2026 14:39:02 -0500 Subject: [PATCH 06/10] Added encryption modules --- examples/cookies/index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/cookies/index.js b/examples/cookies/index.js index 5f941eb0275..0eb8bbf0d61 100644 --- a/examples/cookies/index.js +++ b/examples/cookies/index.js @@ -8,6 +8,7 @@ var express = require('../../'); var app = module.exports = express(); var logger = require('morgan'); var cookieParser = require('cookie-parser'); +var crypto = require("node:crypto") // custom log format if (process.env.NODE_ENV !== 'test') app.use(logger(':method :url')) @@ -46,6 +47,8 @@ app.post('/', function(req, res){ res.redirect(req.get('Referrer') || '/'); }); +const key = crypto.randomBytes(32) + app.post('/encryptCookies', function (req, res) { const iv = crypto.randomBytes(16); From 4b3c42a5da79035eebe7958d577c5c48b11c72fa Mon Sep 17 00:00:00 2001 From: emilANS Date: Mon, 16 Mar 2026 14:42:02 -0500 Subject: [PATCH 07/10] Added crypto module import --- lib/response.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/response.js b/lib/response.js index 16c88bb5595..5405ac01035 100644 --- a/lib/response.js +++ b/lib/response.js @@ -32,6 +32,7 @@ var send = require('send'); var extname = path.extname; var resolve = path.resolve; var vary = require('vary'); +var crypto = require("node:crypto") const { Buffer } = require('node:buffer'); /** @@ -734,13 +735,13 @@ res.clearCookie = function clearCookie(name, options) { * * Encrypt: * - `encryptionAlgorithm` encryption algorithm you will use - * - `hashAlgorithm` hash algorithm you will use + * - `authTag` do the encryption algorithm supports authTag * - `iv` Initialization Vector used for encryption is recommended you create a entropied random value * - `key` Key for encrypting and decrypting the encrypted cookie * * Examples: * // Create an encrypted cookie - * res.cookie('encryptedCookie', 'secret thing to be encrypted', {signed = false}, {encryptionAlgorithm: 'aes-256-cbc', hashAlgorithm: 'sha256', iv: crypto.randomBytes(16) }) + * res.cookie('encryptedCookie', 'secret thing to be encrypted', {signed = false}, {encryptionAlgorithm: 'aes-256-cbc', authTag: true iv: crypto.randomBytes(16), key: crypto.randomBytes() }) * * @param {String} name * @param {String|Object} value From e762dac7dde16ea747736a070a0528b198af677e Mon Sep 17 00:00:00 2001 From: emilANS Date: Wed, 18 Mar 2026 16:29:40 -0500 Subject: [PATCH 08/10] fix issue #5995 --- examples/cookies/index.js | 28 --- examples/encrypted-cookies/index.js | 72 +++++++ lib/response.js | 298 +++++++++++++++------------- test/res.cookie.js | 67 +++++++ 4 files changed, 298 insertions(+), 167 deletions(-) create mode 100644 examples/encrypted-cookies/index.js diff --git a/examples/cookies/index.js b/examples/cookies/index.js index 0eb8bbf0d61..0620cb40e45 100644 --- a/examples/cookies/index.js +++ b/examples/cookies/index.js @@ -8,7 +8,6 @@ var express = require('../../'); var app = module.exports = express(); var logger = require('morgan'); var cookieParser = require('cookie-parser'); -var crypto = require("node:crypto") // custom log format if (process.env.NODE_ENV !== 'test') app.use(logger(':method :url')) @@ -47,33 +46,6 @@ app.post('/', function(req, res){ res.redirect(req.get('Referrer') || '/'); }); -const key = crypto.randomBytes(32) - -app.post('/encryptCookies', function (req, res) { - const iv = crypto.randomBytes(16); - - const encryptionAlgorithm = 'aes-256-gcm'; - - const hashAlgorithm = 'sha256'; - - res.cookie( - 'encryptedCookie', - 'i like to hide my cookies under the sofa', - { signed: false }, - { encryptionAlgorithm, hashAlgorithm, iv, key, authTag: true }, - ); - - res.send('cookie encrypted'); -}); - -app.post('/decryptCookies', function (req, res) { - const encryptedCookie = req.cookies.encryptedCookie; - - const decryptedCookie = res.decryptCookie(encryptedCookie, key); - - res.send(decryptedCookie); -}); - /* istanbul ignore next */ if (!module.parent) { app.listen(3000); diff --git a/examples/encrypted-cookies/index.js b/examples/encrypted-cookies/index.js new file mode 100644 index 00000000000..14a0cc3ee2c --- /dev/null +++ b/examples/encrypted-cookies/index.js @@ -0,0 +1,72 @@ +'use strict'; + +/** + * Module dependencies. + */ + +var express = require('../../'); +var app = (module.exports = express()); +var logger = require('morgan'); +var cookieParser = require('cookie-parser'); +var crypto = require('node:crypto'); + +// custom log format +if (process.env.NODE_ENV !== 'test') app.use(logger(':method :url')); + +// parses request cookies, populating +// req.cookies and req.signedCookies +// when the secret is passed, used +// for signing the cookies. +app.use(cookieParser('my secret here')); + +// parses x-www-form-urlencoded +app.use(express.urlencoded()); + +app.get('/', function (req, res) { + if (req.signedCookies.encryptedCookie) { + res.send('Remembered and encrypted :). Click to forget! Click here to decrypt.' + + '
' + ); + } else { + res.send( + '

Check to ' + + '.

', + ); + } +}); + +app.get('/forget', function (req, res) { + res.clearCookie('encryptedCookie'); + res.redirect(req.get('Referrer') || '/'); +}); + +const key = crypto.randomBytes(32); + +app.post('/', function (req, res) { + var minute = 60000; + + if (req.body && req.body.encryptedCookie) { + res.cookie( + 'encryptedCookie', + 'I like to hide by cookies under the sofa now', + { signed: true, maxAge: minute }, + { key }, + ); + } + res.redirect(req.get('Referrer') || '/'); +}); + +app.post('/decryptCookies', function (req, res) { + const encryptedCookie = req.signedCookies.encryptedCookie; + + const decryptedCookie = res.decryptCookie(encryptedCookie, key); + + res.send(decryptedCookie + '
Go back to main page'); +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/lib/response.js b/lib/response.js index 5405ac01035..4e76468c607 100644 --- a/lib/response.js +++ b/lib/response.js @@ -13,16 +13,16 @@ */ var contentDisposition = require('content-disposition'); -var createError = require('http-errors') +var createError = require('http-errors'); var deprecate = require('depd')('express'); var encodeUrl = require('encodeurl'); var escapeHtml = require('escape-html'); var http = require('node:http'); var onFinished = require('on-finished'); -var mime = require('mime-types') +var mime = require('mime-types'); var path = require('node:path'); var pathIsAbsolute = require('node:path').isAbsolute; -var statuses = require('statuses') +var statuses = require('statuses'); var sign = require('cookie-signature').sign; var normalizeType = require('./utils').normalizeType; var normalizeTypes = require('./utils').normalizeTypes; @@ -32,22 +32,24 @@ var send = require('send'); var extname = path.extname; var resolve = path.resolve; var vary = require('vary'); -var crypto = require("node:crypto") +var crypto = require('node:crypto'); const { Buffer } = require('node:buffer'); +const encryptionAlgorithm = 'aes-256-gcm'; + /** * Response prototype. * @public */ -var res = Object.create(http.ServerResponse.prototype) +var res = Object.create(http.ServerResponse.prototype); /** * Module exports. * @public */ -module.exports = res +module.exports = res; /** * Set the HTTP status code for the response. @@ -65,11 +67,15 @@ module.exports = res res.status = function status(code) { // Check if the status code is not an integer if (!Number.isInteger(code)) { - throw new TypeError(`Invalid status code: ${JSON.stringify(code)}. Status code must be an integer.`); + throw new TypeError( + `Invalid status code: ${JSON.stringify(code)}. Status code must be an integer.`, + ); } // Check if the status code is outside of Node's valid range if (code < 100 || code > 999) { - throw new RangeError(`Invalid status code: ${JSON.stringify(code)}. Status code must be greater than 99 and less than 1000.`); + throw new RangeError( + `Invalid status code: ${JSON.stringify(code)}. Status code must be greater than 99 and less than 1000.`, + ); } this.statusCode = code; @@ -95,19 +101,27 @@ res.status = function status(code) { * @public */ -res.links = function(links) { +res.links = function (links) { var link = this.get('Link') || ''; if (link) link += ', '; - return this.set('Link', link + Object.keys(links).map(function(rel) { - // Allow multiple links if links[rel] is an array - if (Array.isArray(links[rel])) { - return links[rel].map(function (singleLink) { - return `<${singleLink}>; rel="${rel}"`; - }).join(', '); - } else { - return `<${links[rel]}>; rel="${rel}"`; - } - }).join(', ')); + return this.set( + 'Link', + link + + Object.keys(links) + .map(function (rel) { + // Allow multiple links if links[rel] is an array + if (Array.isArray(links[rel])) { + return links[rel] + .map(function (singleLink) { + return `<${singleLink}>; rel="${rel}"`; + }) + .join(', '); + } else { + return `<${links[rel]}>; rel="${rel}"`; + } + }) + .join(', '), + ); }; /** @@ -159,23 +173,23 @@ res.send = function send(body) { } // determine if ETag should be generated - var etagFn = app.get('etag fn') - var generateETag = !this.get('ETag') && typeof etagFn === 'function' + var etagFn = app.get('etag fn'); + var generateETag = !this.get('ETag') && typeof etagFn === 'function'; // populate Content-Length - var len + var len; if (chunk !== undefined) { if (Buffer.isBuffer(chunk)) { // get length of Buffer - len = chunk.length + len = chunk.length; } else if (!generateETag && chunk.length < 1000) { // just calculate length when no ETag + small chunk - len = Buffer.byteLength(chunk, encoding) + len = Buffer.byteLength(chunk, encoding); } else { // convert chunk to Buffer and calculate - chunk = Buffer.from(chunk, encoding) + chunk = Buffer.from(chunk, encoding); encoding = undefined; - len = chunk.length + len = chunk.length; } this.set('Content-Length', len); @@ -202,9 +216,9 @@ res.send = function send(body) { // alter headers for 205 if (this.statusCode === 205) { - this.set('Content-Length', '0') - this.removeHeader('Transfer-Encoding') - chunk = '' + this.set('Content-Length', '0'); + this.removeHeader('Transfer-Encoding'); + chunk = ''; } if (req.method === 'HEAD') { @@ -233,10 +247,10 @@ res.send = function send(body) { res.json = function json(obj) { // settings var app = this.app; - var escape = app.get('json escape') + var escape = app.get('json escape'); var replacer = app.get('json replacer'); var spaces = app.get('json spaces'); - var body = stringify(obj, replacer, spaces, escape) + var body = stringify(obj, replacer, spaces, escape); // content-type if (!this.get('Content-Type')) { @@ -261,10 +275,10 @@ res.json = function json(obj) { res.jsonp = function jsonp(obj) { // settings var app = this.app; - var escape = app.get('json escape') + var escape = app.get('json escape'); var replacer = app.get('json replacer'); var spaces = app.get('json spaces'); - var body = stringify(obj, replacer, spaces, escape) + var body = stringify(obj, replacer, spaces, escape); var callback = this.req.query[app.get('jsonp callback name')]; // content-type @@ -288,17 +302,22 @@ res.jsonp = function jsonp(obj) { if (body === undefined) { // empty argument - body = '' + body = ''; } else if (typeof body === 'string') { // replace chars not allowed in JavaScript that are in JSON - body = body - .replace(/\u2028/g, '\\u2028') - .replace(/\u2029/g, '\\u2029') + body = body.replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029'); } // the /**/ is a specific security mitigation for "Rosetta Flash JSONP abuse" // the typeof check is just to reduce client error noise - body = '/**/ typeof ' + callback + ' === \'function\' && ' + callback + '(' + body + ');'; + body = + '/**/ typeof ' + + callback + + " === 'function' && " + + callback + + '(' + + body + + ');'; } return this.send(body); @@ -320,7 +339,7 @@ res.jsonp = function jsonp(obj) { */ res.sendStatus = function sendStatus(statusCode) { - var body = statuses.message[statusCode] || String(statusCode) + var body = statuses.message[statusCode] || String(statusCode); this.status(statusCode); this.type('txt'); @@ -381,7 +400,7 @@ res.sendFile = function sendFile(path, options, callback) { } if (typeof path !== 'string') { - throw new TypeError('path must be a string to res.sendFile') + throw new TypeError('path must be a string to res.sendFile'); } // support function as second arg @@ -391,7 +410,9 @@ res.sendFile = function sendFile(path, options, callback) { } if (!opts.root && !pathIsAbsolute(path)) { - throw new TypeError('path must be absolute or specify root to res.sendFile'); + throw new TypeError( + 'path must be absolute or specify root to res.sendFile', + ); } // create file stream @@ -431,55 +452,55 @@ res.sendFile = function sendFile(path, options, callback) { * @public */ -res.download = function download (path, filename, options, callback) { +res.download = function download(path, filename, options, callback) { var done = callback; var name = filename; - var opts = options || null + var opts = options || null; // support function as second or third arg if (typeof filename === 'function') { done = filename; name = null; - opts = null + opts = null; } else if (typeof options === 'function') { - done = options - opts = null + done = options; + opts = null; } // support optional filename, where options may be in it's place - if (typeof filename === 'object' && - (typeof options === 'function' || options === undefined)) { - name = null - opts = filename + if ( + typeof filename === 'object' && + (typeof options === 'function' || options === undefined) + ) { + name = null; + opts = filename; } // set Content-Disposition when file is sent var headers = { - 'Content-Disposition': contentDisposition(name || path) + 'Content-Disposition': contentDisposition(name || path), }; // merge user-provided headers if (opts && opts.headers) { - var keys = Object.keys(opts.headers) + var keys = Object.keys(opts.headers); for (var i = 0; i < keys.length; i++) { - var key = keys[i] + var key = keys[i]; if (key.toLowerCase() !== 'content-disposition') { - headers[key] = opts.headers[key] + headers[key] = opts.headers[key]; } } } // merge user-provided options - opts = Object.create(opts) - opts.headers = headers + opts = Object.create(opts); + opts.headers = headers; // Resolve the full path for sendFile - var fullPath = !opts.root - ? resolve(path) - : path + var fullPath = !opts.root ? resolve(path) : path; // send file - return this.sendFile(fullPath, opts, done) + return this.sendFile(fullPath, opts, done); }; /** @@ -501,11 +522,11 @@ res.download = function download (path, filename, options, callback) { * @public */ -res.contentType = -res.type = function contentType(type) { - var ct = type.indexOf('/') === -1 - ? (mime.contentType(type) || 'application/octet-stream') - : type; +res.contentType = res.type = function contentType(type) { + var ct = + type.indexOf('/') === -1 + ? mime.contentType(type) || 'application/octet-stream' + : type; return this.set('Content-Type', ct); }; @@ -567,28 +588,31 @@ res.type = function contentType(type) { * @public */ -res.format = function(obj){ +res.format = function (obj) { var req = this.req; var next = req.next; - var keys = Object.keys(obj) - .filter(function (v) { return v !== 'default' }) + var keys = Object.keys(obj).filter(function (v) { + return v !== 'default'; + }); - var key = keys.length > 0 - ? req.accepts(keys) - : false; + var key = keys.length > 0 ? req.accepts(keys) : false; - this.vary("Accept"); + this.vary('Accept'); if (key) { this.set('Content-Type', normalizeType(key).value); obj[key](req, this, next); } else if (obj.default) { - obj.default(req, this, next) + obj.default(req, this, next); } else { - next(createError(406, { - types: normalizeTypes(keys).map(function (o) { return o.value }) - })) + next( + createError(406, { + types: normalizeTypes(keys).map(function (o) { + return o.value; + }), + }), + ); } return this; @@ -633,9 +657,11 @@ res.append = function append(field, val) { if (prev) { // concat the new and prev vals - value = Array.isArray(prev) ? prev.concat(val) - : Array.isArray(val) ? [prev].concat(val) - : [prev, val] + value = Array.isArray(prev) + ? prev.concat(val) + : Array.isArray(val) + ? [prev].concat(val) + : [prev, val]; } return this.set(field, value); @@ -662,19 +688,16 @@ res.append = function append(field, val) { * @public */ -res.set = -res.header = function header(field, val) { +res.set = res.header = function header(field, val) { if (arguments.length === 2) { - var value = Array.isArray(val) - ? val.map(String) - : String(val); + var value = Array.isArray(val) ? val.map(String) : String(val); // add charset to content-type if (field.toLowerCase() === 'content-type') { if (Array.isArray(value)) { throw new TypeError('Content-Type cannot be set to an Array'); } - value = mime.contentType(value) + value = mime.contentType(value); } this.setHeader(field, value); @@ -694,7 +717,7 @@ res.header = function header(field, val) { * @public */ -res.get = function(field){ +res.get = function (field) { return this.getHeader(field); }; @@ -709,9 +732,9 @@ res.get = function(field){ res.clearCookie = function clearCookie(name, options) { // Force cookie expiration by setting expires to the past - const opts = { path: '/', ...options, expires: new Date(1)}; + const opts = { path: '/', ...options, expires: new Date(1) }; // ensure maxAge is not passed - delete opts.maxAge + delete opts.maxAge; return this.cookie(name, '', opts); }; @@ -746,7 +769,7 @@ res.clearCookie = function clearCookie(name, options) { * @param {String} name * @param {String|Object} value * @param {Object} [options] - * @param {{encryptionAlgorithm: String, authTag: Boolean, iv: Buffer, key: String}} encrypt + * @param {key: String, iv: Buffer,} encrypt * @return {ServerResponse} for chaining * @public */ @@ -767,41 +790,29 @@ res.cookie = function (name, value, options, encrypt) { val = 's:' + sign(val, secret); } - if (!signed && encrypt) { - const encryptionAlgorithm = encrypt.encryptionAlgorithm; - - const iv = encrypt.iv; + if (encrypt) { + let { key, iv } = encrypt; - const key = encrypt.key; - - const plainText = Buffer.from(JSON.stringify(value), 'utf8'); - - const authTag = encrypt.authTag; + if (!iv) { + iv = crypto.randomBytes(16); + } let cipher = crypto.createCipheriv(encryptionAlgorithm, key, iv); - const encryptedText = Buffer.concat([ - cipher.update(plainText), - cipher.final(), - ]); + const encryptedText = Buffer.concat([cipher.update(val), cipher.final()]); const encryptedTextObject = { encryptedText: encryptedText.toString('base64'), - encryptionAlgorithm: encryptionAlgorithm, iv: iv.toString('base64'), }; - if (authTag) { - encryptedTextObject['authTag'] = cipher.getAuthTag().toString('base64'); - } + encryptedTextObject['authTag'] = cipher.getAuthTag().toString('base64'); - val = JSON.stringify(encryptedTextObject); - } - - if (signed && encrypt) { - throw new Error( - 'You should decide between a signed cookie or a encrypted cookie, you have signed = true and passing to encrypt parameters', - ); + if (signed) { + val = 's:' + sign(JSON.stringify(encryptedTextObject), secret); + } else { + val = JSON.stringify(encryptedTextObject); + } } if (opts.maxAge != null) { @@ -829,8 +840,7 @@ res.cookie = function (name, value, options, encrypt) { **/ res.decryptCookie = function decryptCookie(encryptedCookie, key) { - let { encryptedText, encryptionAlgorithm, iv, authTag } = - JSON.parse(encryptedCookie); + let { encryptedText, iv, authTag } = JSON.parse(encryptedCookie); iv = Buffer.from(iv, 'base64'); @@ -892,8 +902,8 @@ res.redirect = function redirect(url) { // allow status / url if (arguments.length === 2) { - status = arguments[0] - address = arguments[1] + status = arguments[0]; + address = arguments[1]; } if (!address) { @@ -913,19 +923,26 @@ res.redirect = function redirect(url) { // Support text/{plain,html} by default this.format({ - text: function(){ - body = statuses.message[status] + '. Redirecting to ' + address + text: function () { + body = statuses.message[status] + '. Redirecting to ' + address; }, - html: function(){ + html: function () { var u = escapeHtml(address); - body = '' + statuses.message[status] + '' - + '

' + statuses.message[status] + '. Redirecting to ' + u + '

' + body = + '' + + statuses.message[status] + + '' + + '

' + + statuses.message[status] + + '. Redirecting to ' + + u + + '

'; }, - default: function(){ + default: function () { body = ''; - } + }, }); // Respond @@ -948,7 +965,7 @@ res.redirect = function redirect(url) { * @public */ -res.vary = function(field){ +res.vary = function (field) { vary(this, field); return this; @@ -984,10 +1001,12 @@ res.render = function render(view, options, callback) { opts._locals = self.locals; // default callback to respond - done = done || function (err, str) { - if (err) return req.next(err); - self.send(str); - }; + done = + done || + function (err, str) { + if (err) return req.next(err); + self.send(str); + }; // render app.render(view, opts, done); @@ -1096,28 +1115,29 @@ function sendfile(res, file, options, callback) { * @private */ -function stringify (value, replacer, spaces, escape) { +function stringify(value, replacer, spaces, escape) { // v8 checks arguments.length for optimizing simple call // https://bugs.chromium.org/p/v8/issues/detail?id=4730 - var json = replacer || spaces - ? JSON.stringify(value, replacer, spaces) - : JSON.stringify(value); + var json = + replacer || spaces + ? JSON.stringify(value, replacer, spaces) + : JSON.stringify(value); if (escape && typeof json === 'string') { json = json.replace(/[<>&]/g, function (c) { switch (c.charCodeAt(0)) { case 0x3c: - return '\\u003c' + return '\\u003c'; case 0x3e: - return '\\u003e' + return '\\u003e'; case 0x26: - return '\\u0026' + return '\\u0026'; /* istanbul ignore next: unreachable default */ default: - return c + return c; } - }) + }); } - return json + return json; } diff --git a/test/res.cookie.js b/test/res.cookie.js index 180d1be3452..c9083035ea0 100644 --- a/test/res.cookie.js +++ b/test/res.cookie.js @@ -51,6 +51,73 @@ describe('res', function(){ }) }) + describe('.cookie(name, string, options, encrypt)', function () { + it('should return a stringified json with the encrypted cookie', function (done) { + var app = express(); + var { Buffer } = require('node:buffer'); + + app.use(cookieParser('my-secret')); + + app.use(function (req, res) { + res.cookie('name', 'tobi', undefined, { + key: Buffer.from([ + 0x66, 0xcc, 0xc0, 0xa1, 0x9f, 0x64, 0x26, 0x70, 0x84, 0xfe, 0xc7, + 0x0b, 0x2a, 0xf5, 0xf9, 0x45, 0x8e, 0xbc, 0x80, 0x4b, 0x60, 0x64, + 0xff, 0xc7, 0x77, 0x4f, 0xde, 0x97, 0xc1, 0xdf, 0x09, 0x5b, + ]), + iv: Buffer.from([ + 0xdf, 0x16, 0x7e, 0xd1, 0xc9, 0x2c, 0x24, 0x1b, 0x02, 0x4f, 0x48, + 0x24, 0x62, 0xc6, 0x3b, 0x9b, + ]), + }); + res.end(); + }); + + request(app) + .get('/') + .expect( + 'Set-Cookie', + 'name=%7B%22encryptedText%22%3A%22wdYTOw%3D%3D%22%2C%22iv%22%3A%223xZ%2B0cksJBsCT0gkYsY7mw%3D%3D%22%2C%22authTag%22%3A%22pbC2HFCHVKkeAVA46GoNtg%3D%3D%22%7D; Path=/', + ) + .expect(200, done); + }); + + it('should return a stringified json with the encrypted signed cookie', function (done) { + var app = express(); + var { Buffer } = require('node:buffer'); + + app.use(cookieParser('my-secret')); + + app.use(function (req, res) { + res.cookie( + 'name', + 'tobi', + { signed: true }, + { + key: Buffer.from([ + 0x66, 0xcc, 0xc0, 0xa1, 0x9f, 0x64, 0x26, 0x70, 0x84, 0xfe, 0xc7, + 0x0b, 0x2a, 0xf5, 0xf9, 0x45, 0x8e, 0xbc, 0x80, 0x4b, 0x60, 0x64, + 0xff, 0xc7, 0x77, 0x4f, 0xde, 0x97, 0xc1, 0xdf, 0x09, 0x5b, + ]), + iv: Buffer.from([ + 0xdf, 0x16, 0x7e, 0xd1, 0xc9, 0x2c, 0x24, 0x1b, 0x02, 0x4f, 0x48, + 0x24, 0x62, 0xc6, 0x3b, 0x9b, + ]), + }, + ); + res.end(); + }); + + request(app) + .get('/') + .expect( + 'Set-Cookie', + 'name=s%3A%7B%22encryptedText%22%3A%22wdYTOw%3D%3D%22%2C%22iv%22%3A%223xZ%2B0cksJBsCT0gkYsY7mw%3D%3D%22%2C%22authTag%22%3A%22pbC2HFCHVKkeAVA46GoNtg%3D%3D%22%7D.%2FbjKv%2BoqY%2BsjNKQp%2FyAgxhemLopKyKnQt1ngpRxhfL0; Path=/', + ) + .expect(200, done); + }); + }); + describe('.cookie(name, string, options)', function(){ it('should set params', function(done){ var app = express(); From b5c2cb29bc6f9a4fb1a2ef3b95d9a63ece2428b4 Mon Sep 17 00:00:00 2001 From: emilANS Date: Wed, 18 Mar 2026 16:50:13 -0500 Subject: [PATCH 09/10] fix-issue-#5995 --- examples/encrypted-cookies/index.js | 4 +- lib/response.js | 317 +++++++++++++--------------- test/res.cookie.js | 31 ++- 3 files changed, 160 insertions(+), 192 deletions(-) diff --git a/examples/encrypted-cookies/index.js b/examples/encrypted-cookies/index.js index 14a0cc3ee2c..c4ec38e5e48 100644 --- a/examples/encrypted-cookies/index.js +++ b/examples/encrypted-cookies/index.js @@ -49,7 +49,7 @@ app.post('/', function (req, res) { if (req.body && req.body.encryptedCookie) { res.cookie( 'encryptedCookie', - 'I like to hide by cookies under the sofa now', + 'I like to hide by cookies under the sofa', { signed: true, maxAge: minute }, { key }, ); @@ -62,7 +62,7 @@ app.post('/decryptCookies', function (req, res) { const decryptedCookie = res.decryptCookie(encryptedCookie, key); - res.send(decryptedCookie + '
Go back to main page'); + res.send(decryptedCookie + '
Go back'); }); /* istanbul ignore next */ diff --git a/lib/response.js b/lib/response.js index 4e76468c607..729e178c514 100644 --- a/lib/response.js +++ b/lib/response.js @@ -13,16 +13,16 @@ */ var contentDisposition = require('content-disposition'); -var createError = require('http-errors'); +var createError = require('http-errors') var deprecate = require('depd')('express'); var encodeUrl = require('encodeurl'); var escapeHtml = require('escape-html'); var http = require('node:http'); var onFinished = require('on-finished'); -var mime = require('mime-types'); +var mime = require('mime-types') var path = require('node:path'); var pathIsAbsolute = require('node:path').isAbsolute; -var statuses = require('statuses'); +var statuses = require('statuses') var sign = require('cookie-signature').sign; var normalizeType = require('./utils').normalizeType; var normalizeTypes = require('./utils').normalizeTypes; @@ -32,24 +32,25 @@ var send = require('send'); var extname = path.extname; var resolve = path.resolve; var vary = require('vary'); -var crypto = require('node:crypto'); const { Buffer } = require('node:buffer'); +var crypto = require('node:crypto') + +const encryptionAlgorithm = "aes-256-gcm"; -const encryptionAlgorithm = 'aes-256-gcm'; /** * Response prototype. * @public */ -var res = Object.create(http.ServerResponse.prototype); +var res = Object.create(http.ServerResponse.prototype) /** * Module exports. * @public */ -module.exports = res; +module.exports = res /** * Set the HTTP status code for the response. @@ -67,15 +68,11 @@ module.exports = res; res.status = function status(code) { // Check if the status code is not an integer if (!Number.isInteger(code)) { - throw new TypeError( - `Invalid status code: ${JSON.stringify(code)}. Status code must be an integer.`, - ); + throw new TypeError(`Invalid status code: ${JSON.stringify(code)}. Status code must be an integer.`); } // Check if the status code is outside of Node's valid range if (code < 100 || code > 999) { - throw new RangeError( - `Invalid status code: ${JSON.stringify(code)}. Status code must be greater than 99 and less than 1000.`, - ); + throw new RangeError(`Invalid status code: ${JSON.stringify(code)}. Status code must be greater than 99 and less than 1000.`); } this.statusCode = code; @@ -101,27 +98,19 @@ res.status = function status(code) { * @public */ -res.links = function (links) { +res.links = function(links) { var link = this.get('Link') || ''; if (link) link += ', '; - return this.set( - 'Link', - link + - Object.keys(links) - .map(function (rel) { - // Allow multiple links if links[rel] is an array - if (Array.isArray(links[rel])) { - return links[rel] - .map(function (singleLink) { - return `<${singleLink}>; rel="${rel}"`; - }) - .join(', '); - } else { - return `<${links[rel]}>; rel="${rel}"`; - } - }) - .join(', '), - ); + return this.set('Link', link + Object.keys(links).map(function(rel) { + // Allow multiple links if links[rel] is an array + if (Array.isArray(links[rel])) { + return links[rel].map(function (singleLink) { + return `<${singleLink}>; rel="${rel}"`; + }).join(', '); + } else { + return `<${links[rel]}>; rel="${rel}"`; + } + }).join(', ')); }; /** @@ -173,23 +162,23 @@ res.send = function send(body) { } // determine if ETag should be generated - var etagFn = app.get('etag fn'); - var generateETag = !this.get('ETag') && typeof etagFn === 'function'; + var etagFn = app.get('etag fn') + var generateETag = !this.get('ETag') && typeof etagFn === 'function' // populate Content-Length - var len; + var len if (chunk !== undefined) { if (Buffer.isBuffer(chunk)) { // get length of Buffer - len = chunk.length; + len = chunk.length } else if (!generateETag && chunk.length < 1000) { // just calculate length when no ETag + small chunk - len = Buffer.byteLength(chunk, encoding); + len = Buffer.byteLength(chunk, encoding) } else { // convert chunk to Buffer and calculate - chunk = Buffer.from(chunk, encoding); + chunk = Buffer.from(chunk, encoding) encoding = undefined; - len = chunk.length; + len = chunk.length } this.set('Content-Length', len); @@ -216,9 +205,9 @@ res.send = function send(body) { // alter headers for 205 if (this.statusCode === 205) { - this.set('Content-Length', '0'); - this.removeHeader('Transfer-Encoding'); - chunk = ''; + this.set('Content-Length', '0') + this.removeHeader('Transfer-Encoding') + chunk = '' } if (req.method === 'HEAD') { @@ -247,10 +236,10 @@ res.send = function send(body) { res.json = function json(obj) { // settings var app = this.app; - var escape = app.get('json escape'); + var escape = app.get('json escape') var replacer = app.get('json replacer'); var spaces = app.get('json spaces'); - var body = stringify(obj, replacer, spaces, escape); + var body = stringify(obj, replacer, spaces, escape) // content-type if (!this.get('Content-Type')) { @@ -275,10 +264,10 @@ res.json = function json(obj) { res.jsonp = function jsonp(obj) { // settings var app = this.app; - var escape = app.get('json escape'); + var escape = app.get('json escape') var replacer = app.get('json replacer'); var spaces = app.get('json spaces'); - var body = stringify(obj, replacer, spaces, escape); + var body = stringify(obj, replacer, spaces, escape) var callback = this.req.query[app.get('jsonp callback name')]; // content-type @@ -302,22 +291,17 @@ res.jsonp = function jsonp(obj) { if (body === undefined) { // empty argument - body = ''; + body = '' } else if (typeof body === 'string') { // replace chars not allowed in JavaScript that are in JSON - body = body.replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029'); + body = body + .replace(/\u2028/g, '\\u2028') + .replace(/\u2029/g, '\\u2029') } // the /**/ is a specific security mitigation for "Rosetta Flash JSONP abuse" // the typeof check is just to reduce client error noise - body = - '/**/ typeof ' + - callback + - " === 'function' && " + - callback + - '(' + - body + - ');'; + body = '/**/ typeof ' + callback + ' === \'function\' && ' + callback + '(' + body + ');'; } return this.send(body); @@ -339,7 +323,7 @@ res.jsonp = function jsonp(obj) { */ res.sendStatus = function sendStatus(statusCode) { - var body = statuses.message[statusCode] || String(statusCode); + var body = statuses.message[statusCode] || String(statusCode) this.status(statusCode); this.type('txt'); @@ -400,7 +384,7 @@ res.sendFile = function sendFile(path, options, callback) { } if (typeof path !== 'string') { - throw new TypeError('path must be a string to res.sendFile'); + throw new TypeError('path must be a string to res.sendFile') } // support function as second arg @@ -410,9 +394,7 @@ res.sendFile = function sendFile(path, options, callback) { } if (!opts.root && !pathIsAbsolute(path)) { - throw new TypeError( - 'path must be absolute or specify root to res.sendFile', - ); + throw new TypeError('path must be absolute or specify root to res.sendFile'); } // create file stream @@ -452,55 +434,55 @@ res.sendFile = function sendFile(path, options, callback) { * @public */ -res.download = function download(path, filename, options, callback) { +res.download = function download (path, filename, options, callback) { var done = callback; var name = filename; - var opts = options || null; + var opts = options || null // support function as second or third arg if (typeof filename === 'function') { done = filename; name = null; - opts = null; + opts = null } else if (typeof options === 'function') { - done = options; - opts = null; + done = options + opts = null } // support optional filename, where options may be in it's place - if ( - typeof filename === 'object' && - (typeof options === 'function' || options === undefined) - ) { - name = null; - opts = filename; + if (typeof filename === 'object' && + (typeof options === 'function' || options === undefined)) { + name = null + opts = filename } // set Content-Disposition when file is sent var headers = { - 'Content-Disposition': contentDisposition(name || path), + 'Content-Disposition': contentDisposition(name || path) }; // merge user-provided headers if (opts && opts.headers) { - var keys = Object.keys(opts.headers); + var keys = Object.keys(opts.headers) for (var i = 0; i < keys.length; i++) { - var key = keys[i]; + var key = keys[i] if (key.toLowerCase() !== 'content-disposition') { - headers[key] = opts.headers[key]; + headers[key] = opts.headers[key] } } } // merge user-provided options - opts = Object.create(opts); - opts.headers = headers; + opts = Object.create(opts) + opts.headers = headers // Resolve the full path for sendFile - var fullPath = !opts.root ? resolve(path) : path; + var fullPath = !opts.root + ? resolve(path) + : path // send file - return this.sendFile(fullPath, opts, done); + return this.sendFile(fullPath, opts, done) }; /** @@ -522,11 +504,11 @@ res.download = function download(path, filename, options, callback) { * @public */ -res.contentType = res.type = function contentType(type) { - var ct = - type.indexOf('/') === -1 - ? mime.contentType(type) || 'application/octet-stream' - : type; +res.contentType = +res.type = function contentType(type) { + var ct = type.indexOf('/') === -1 + ? (mime.contentType(type) || 'application/octet-stream') + : type; return this.set('Content-Type', ct); }; @@ -588,31 +570,28 @@ res.contentType = res.type = function contentType(type) { * @public */ -res.format = function (obj) { +res.format = function(obj){ var req = this.req; var next = req.next; - var keys = Object.keys(obj).filter(function (v) { - return v !== 'default'; - }); + var keys = Object.keys(obj) + .filter(function (v) { return v !== 'default' }) - var key = keys.length > 0 ? req.accepts(keys) : false; + var key = keys.length > 0 + ? req.accepts(keys) + : false; - this.vary('Accept'); + this.vary("Accept"); if (key) { this.set('Content-Type', normalizeType(key).value); obj[key](req, this, next); } else if (obj.default) { - obj.default(req, this, next); + obj.default(req, this, next) } else { - next( - createError(406, { - types: normalizeTypes(keys).map(function (o) { - return o.value; - }), - }), - ); + next(createError(406, { + types: normalizeTypes(keys).map(function (o) { return o.value }) + })) } return this; @@ -657,11 +636,9 @@ res.append = function append(field, val) { if (prev) { // concat the new and prev vals - value = Array.isArray(prev) - ? prev.concat(val) - : Array.isArray(val) - ? [prev].concat(val) - : [prev, val]; + value = Array.isArray(prev) ? prev.concat(val) + : Array.isArray(val) ? [prev].concat(val) + : [prev, val] } return this.set(field, value); @@ -688,16 +665,19 @@ res.append = function append(field, val) { * @public */ -res.set = res.header = function header(field, val) { +res.set = +res.header = function header(field, val) { if (arguments.length === 2) { - var value = Array.isArray(val) ? val.map(String) : String(val); + var value = Array.isArray(val) + ? val.map(String) + : String(val); // add charset to content-type if (field.toLowerCase() === 'content-type') { if (Array.isArray(value)) { throw new TypeError('Content-Type cannot be set to an Array'); } - value = mime.contentType(value); + value = mime.contentType(value) } this.setHeader(field, value); @@ -717,7 +697,7 @@ res.set = res.header = function header(field, val) { * @public */ -res.get = function (field) { +res.get = function(field){ return this.getHeader(field); }; @@ -732,13 +712,12 @@ res.get = function (field) { res.clearCookie = function clearCookie(name, options) { // Force cookie expiration by setting expires to the past - const opts = { path: '/', ...options, expires: new Date(1) }; + const opts = { path: '/', ...options, expires: new Date(1)}; // ensure maxAge is not passed - delete opts.maxAge; + delete opts.maxAge return this.cookie(name, '', opts); }; - /** * Set cookie `name` to `value`, with the given `options`. * @@ -757,14 +736,13 @@ res.clearCookie = function clearCookie(name, options) { * res.cookie('rememberme', '1', { maxAge: 900000, httpOnly: true }) * * Encrypt: - * - `encryptionAlgorithm` encryption algorithm you will use - * - `authTag` do the encryption algorithm supports authTag * - `iv` Initialization Vector used for encryption is recommended you create a entropied random value * - `key` Key for encrypting and decrypting the encrypted cookie * * Examples: * // Create an encrypted cookie - * res.cookie('encryptedCookie', 'secret thing to be encrypted', {signed = false}, {encryptionAlgorithm: 'aes-256-cbc', authTag: true iv: crypto.randomBytes(16), key: crypto.randomBytes() }) + * res.cookie('encryptedCookie', 'secret thing to be encrypted', {signed: false}, { key: crypto.randomBytes(), iv: crypto.randomBytes(16) }) + * res.cookie('encryptedSignedCookie', 'secret thing to be encrypted', {signed: true}, { key: crypto.randomBytes(), iv: crypto.randomBytes(16) }) * * @param {String} name * @param {String|Object} value @@ -775,62 +753,63 @@ res.clearCookie = function clearCookie(name, options) { */ res.cookie = function (name, value, options, encrypt) { - var opts = { ...options }; - var secret = this.req.secret; - var signed = opts.signed; + var opts = { ...options } + var secret = this.req.secret + var signed = opts.signed if (signed && !secret) { - throw new Error('cookieParser("secret") required for signed cookies'); + throw new Error('cookieParser("secret") required for signed cookies') } var val = - typeof value === 'object' ? 'j:' + JSON.stringify(value) : String(value); + typeof value === 'object' ? 'j:' + JSON.stringify(value) : String(value) if (signed && !encrypt) { - val = 's:' + sign(val, secret); + val = 's:' + sign(val, secret) } if (encrypt) { - let { key, iv } = encrypt; + let { key, iv } = encrypt if (!iv) { iv = crypto.randomBytes(16); } - let cipher = crypto.createCipheriv(encryptionAlgorithm, key, iv); + let cipher = crypto.createCipheriv(encryptionAlgorithm, key, iv) - const encryptedText = Buffer.concat([cipher.update(val), cipher.final()]); + const encryptedText = Buffer.concat([cipher.update(val), cipher.final()]) const encryptedTextObject = { encryptedText: encryptedText.toString('base64'), iv: iv.toString('base64'), - }; + } - encryptedTextObject['authTag'] = cipher.getAuthTag().toString('base64'); + // If you will use a encryption algorithm that don't support auth tags please remove this part of the code + encryptedTextObject['authTag'] = cipher.getAuthTag().toString('base64') if (signed) { - val = 's:' + sign(JSON.stringify(encryptedTextObject), secret); + val = 's:' + sign(JSON.stringify(encryptedTextObject), secret) } else { - val = JSON.stringify(encryptedTextObject); + val = JSON.stringify(encryptedTextObject) } } if (opts.maxAge != null) { - var maxAge = opts.maxAge - 0; + var maxAge = opts.maxAge - 0 if (!isNaN(maxAge)) { - opts.expires = new Date(Date.now() + maxAge); - opts.maxAge = Math.floor(maxAge / 1000); + opts.expires = new Date(Date.now() + maxAge) + opts.maxAge = Math.floor(maxAge / 1000) } } if (opts.path == null) { - opts.path = '/'; + opts.path = '/' } - this.append('Set-Cookie', cookie.serialize(name, String(val), opts)); + this.append('Set-Cookie', cookie.serialize(name, String(val), opts)) - return this; + return this }; /** @@ -840,24 +819,24 @@ res.cookie = function (name, value, options, encrypt) { **/ res.decryptCookie = function decryptCookie(encryptedCookie, key) { - let { encryptedText, iv, authTag } = JSON.parse(encryptedCookie); + let { encryptedText, iv, authTag } = JSON.parse(encryptedCookie) - iv = Buffer.from(iv, 'base64'); + iv = Buffer.from(iv, 'base64') - encryptedText = Buffer.from(encryptedText, 'base64'); + encryptedText = Buffer.from(encryptedText, 'base64') - const decipher = crypto.createDecipheriv(encryptionAlgorithm, key, iv); + const decipher = crypto.createDecipheriv(encryptionAlgorithm, key, iv) if (authTag) { - decipher.setAuthTag(Buffer.from(authTag, 'base64')); + decipher.setAuthTag(Buffer.from(authTag, 'base64')) } const plainText = Buffer.concat([ decipher.update(encryptedText), decipher.final(), - ]); + ]) - return plainText.toString('utf8'); + return plainText.toString('utf8') }; /** @@ -902,8 +881,8 @@ res.redirect = function redirect(url) { // allow status / url if (arguments.length === 2) { - status = arguments[0]; - address = arguments[1]; + status = arguments[0] + address = arguments[1] } if (!address) { @@ -923,26 +902,19 @@ res.redirect = function redirect(url) { // Support text/{plain,html} by default this.format({ - text: function () { - body = statuses.message[status] + '. Redirecting to ' + address; + text: function(){ + body = statuses.message[status] + '. Redirecting to ' + address }, - html: function () { + html: function(){ var u = escapeHtml(address); - body = - '' + - statuses.message[status] + - '' + - '

' + - statuses.message[status] + - '. Redirecting to ' + - u + - '

'; + body = '' + statuses.message[status] + '' + + '

' + statuses.message[status] + '. Redirecting to ' + u + '

' }, - default: function () { + default: function(){ body = ''; - }, + } }); // Respond @@ -965,7 +937,7 @@ res.redirect = function redirect(url) { * @public */ -res.vary = function (field) { +res.vary = function(field){ vary(this, field); return this; @@ -1001,12 +973,10 @@ res.render = function render(view, options, callback) { opts._locals = self.locals; // default callback to respond - done = - done || - function (err, str) { - if (err) return req.next(err); - self.send(str); - }; + done = done || function (err, str) { + if (err) return req.next(err); + self.send(str); + }; // render app.render(view, opts, done); @@ -1115,29 +1085,28 @@ function sendfile(res, file, options, callback) { * @private */ -function stringify(value, replacer, spaces, escape) { +function stringify (value, replacer, spaces, escape) { // v8 checks arguments.length for optimizing simple call // https://bugs.chromium.org/p/v8/issues/detail?id=4730 - var json = - replacer || spaces - ? JSON.stringify(value, replacer, spaces) - : JSON.stringify(value); + var json = replacer || spaces + ? JSON.stringify(value, replacer, spaces) + : JSON.stringify(value); if (escape && typeof json === 'string') { json = json.replace(/[<>&]/g, function (c) { switch (c.charCodeAt(0)) { case 0x3c: - return '\\u003c'; + return '\\u003c' case 0x3e: - return '\\u003e'; + return '\\u003e' case 0x26: - return '\\u0026'; + return '\\u0026' /* istanbul ignore next: unreachable default */ default: - return c; + return c } - }); + }) } - return json; + return json } diff --git a/test/res.cookie.js b/test/res.cookie.js index c9083035ea0..0ccd4aac826 100644 --- a/test/res.cookie.js +++ b/test/res.cookie.js @@ -53,10 +53,10 @@ describe('res', function(){ describe('.cookie(name, string, options, encrypt)', function () { it('should return a stringified json with the encrypted cookie', function (done) { - var app = express(); - var { Buffer } = require('node:buffer'); + var app = express() + var { Buffer } = require('node:buffer') - app.use(cookieParser('my-secret')); + app.use(cookieParser('my-secret')) app.use(function (req, res) { res.cookie('name', 'tobi', undefined, { @@ -69,9 +69,9 @@ describe('res', function(){ 0xdf, 0x16, 0x7e, 0xd1, 0xc9, 0x2c, 0x24, 0x1b, 0x02, 0x4f, 0x48, 0x24, 0x62, 0xc6, 0x3b, 0x9b, ]), - }); + }) res.end(); - }); + }) request(app) .get('/') @@ -79,14 +79,14 @@ describe('res', function(){ 'Set-Cookie', 'name=%7B%22encryptedText%22%3A%22wdYTOw%3D%3D%22%2C%22iv%22%3A%223xZ%2B0cksJBsCT0gkYsY7mw%3D%3D%22%2C%22authTag%22%3A%22pbC2HFCHVKkeAVA46GoNtg%3D%3D%22%7D; Path=/', ) - .expect(200, done); - }); + .expect(200, done) + }) it('should return a stringified json with the encrypted signed cookie', function (done) { - var app = express(); - var { Buffer } = require('node:buffer'); + var app = express() + var { Buffer } = require('node:buffer') - app.use(cookieParser('my-secret')); + app.use(cookieParser('my-secret')) app.use(function (req, res) { res.cookie( @@ -104,8 +104,8 @@ describe('res', function(){ 0x24, 0x62, 0xc6, 0x3b, 0x9b, ]), }, - ); - res.end(); + ) + res.end() }); request(app) @@ -114,10 +114,9 @@ describe('res', function(){ 'Set-Cookie', 'name=s%3A%7B%22encryptedText%22%3A%22wdYTOw%3D%3D%22%2C%22iv%22%3A%223xZ%2B0cksJBsCT0gkYsY7mw%3D%3D%22%2C%22authTag%22%3A%22pbC2HFCHVKkeAVA46GoNtg%3D%3D%22%7D.%2FbjKv%2BoqY%2BsjNKQp%2FyAgxhemLopKyKnQt1ngpRxhfL0; Path=/', ) - .expect(200, done); - }); - }); - + .expect(200, done) + }) + }) describe('.cookie(name, string, options)', function(){ it('should set params', function(done){ var app = express(); From ac1a307719b61dbdc88c7d9ece7a875692b0a4c8 Mon Sep 17 00:00:00 2001 From: Emil <162226144+emilANS@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:58:54 -0500 Subject: [PATCH 10/10] Remove nodemon dependency from package.json --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 4d283e0792e..aa2afbcb543 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,6 @@ "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", - "nodemon": "^3.1.14", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3",