Repository: node-red/node-red (js)

Issue:
“X-Powered-By: Express” still present in /theme responses
### Current Behavior

When doing requests to /theme paths of the editor-api, the "X-Powered-By" header is present.

Requested URLs:
https://localhost:1880/theme
https://localhost:1880/theme/login/node-red-256-P.png

Response header: 
X-Powered-By: Express

Setting the httpServerOptions doesn't help.

### Expected Behavior

I need to be able to disable the "X-Powered-By" header for all requests because of a security finding.
As I see there were some efforts to disable this by default: [https://github.com/node-red/node-red/commit/a9b252b8fa5ae3cafb1fbadf421095fadeb8fe02](url)

### Steps To Reproduce

Call the given urls and look at the response header.

### Example flow

_No response_

### Environment

- Node-RED version: v3.0.2
- Node.js version: v16.20.0
- npm version: -
- Platform/OS: Linux / Docker
- Browser: Edge / Firefox


Pull request:
Ensure express server options are applied consistently
Fixes #4169

This creates a single utility function for creating express apps to ensure any server configuration is applied consistently.

Patch:
diff --git a/packages/node_modules/@node-red/editor-api/lib/admin/index.js b/packages/node_modules/@node-red/editor-api/lib/admin/index.js
index 8406fa8e9..26eabe65b 100644
--- a/packages/node_modules/@node-red/editor-api/lib/admin/index.js
+++ b/packages/node_modules/@node-red/editor-api/lib/admin/index.js
@@ -14,8 +14,6 @@
  * limitations under the License.
  **/
 
-var express = require("express");
-
 var nodes = require("./nodes");
 var flows = require("./flows");
 var flow = require("./flow");
@@ -37,18 +35,9 @@ module.exports = {
         plugins.init(runtimeAPI);
         diagnostics.init(settings, runtimeAPI);
 
-        var needsPermission = auth.needsPermission;
-
-        var adminApp = express();
-
-        var defaultServerSettings = {
-            "x-powered-by": false
-        }
-        var serverSettings = Object.assign({},defaultServerSettings,settings.httpServerOptions||{});
-        for (var eOption in serverSettings) {
-            adminApp.set(eOption, serverSettings[eOption]);
-        }
+        const needsPermission = auth.needsPermission;
 
+        const adminApp = apiUtil.createExpressApp(settings)
 
         // Flows
         adminApp.get("/flows",needsPermission("flows.read"),flows.get,apiUtil.errorHandler);
diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/index.js b/packages/node_modules/@node-red/editor-api/lib/editor/index.js
index f210d90fe..42be1f270 100644
--- a/packages/node_modules/@node-red/editor-api/lib/editor/index.js
+++ b/packages/node_modules/@node-red/editor-api/lib/editor/index.js
@@ -46,14 +46,15 @@ module.exports = {
         runtimeAPI = _runtimeAPI;
         needsPermission = auth.needsPermission;
         if (!settings.disableEditor) {
-            info.init(runtimeAPI);
+            info.init(settings, runtimeAPI);
             comms.init(server,settings,runtimeAPI);
 
             var ui = require("./ui");
 
             ui.init(runtimeAPI);
 
-            var editorApp = express();
+            const editorApp = apiUtil.createExpressApp(settings)
+
             if (settings.requireHttps === true) {
                 editorApp.enable('trust proxy');
                 editorApp.use(function (req, res, next) {
@@ -86,7 +87,7 @@ module.exports = {
 
             //Projects
             var projects = require("./projects");
-            projects.init(runtimeAPI);
+            projects.init(settings, runtimeAPI);
             editorApp.use("/projects",projects.app());
 
             // Locales
diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/projects.js b/packages/node_modules/@node-red/editor-api/lib/editor/projects.js
index ad505a46e..5d1b2ff26 100644
--- a/packages/node_modules/@node-red/editor-api/lib/editor/projects.js
+++ b/packages/node_modules/@node-red/editor-api/lib/editor/projects.js
@@ -14,9 +14,9 @@
  * limitations under the License.
  **/
 
-var express = require("express");
 var apiUtils = require("../util");
 
+var settings;
 var runtimeAPI;
 var needsPermission = require("../auth").needsPermission;
 
@@ -77,11 +77,12 @@ function getProjectRemotes(req,res) {
     })
 }
 module.exports = {
-    init: function(_runtimeAPI) {
+    init: function(_settings, _runtimeAPI) {
+        settings = _settings;
         runtimeAPI = _runtimeAPI;
     },
     app: function() {
-        var app = express();
+        var app = apiUtils.createExpressApp(settings)
 
         app.use(function(req,res,next) {
             runtimeAPI.projects.available().then(function(available) {
diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/settings.js b/packages/node_modules/@node-red/editor-api/lib/editor/settings.js
index 5fa2476e1..200ddf2c2 100644
--- a/packages/node_modules/@node-red/editor-api/lib/editor/settings.js
+++ b/packages/node_modules/@node-red/editor-api/lib/editor/settings.js
@@ -18,9 +18,9 @@ var runtimeAPI;
 var sshkeys = require("./sshkeys");
 
 module.exports = {
-    init: function(_runtimeAPI) {
+    init: function(settings, _runtimeAPI) {
         runtimeAPI = _runtimeAPI;
-        sshkeys.init(runtimeAPI);
+        sshkeys.init(settings, runtimeAPI);
     },
     userSettings: function(req, res) {
         var opts = {
diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/sshkeys.js b/packages/node_modules/@node-red/editor-api/lib/editor/sshkeys.js
index 6d1c62e11..08097571f 100644
--- a/packages/node_modules/@node-red/editor-api/lib/editor/sshkeys.js
+++ b/packages/node_modules/@node-red/editor-api/lib/editor/sshkeys.js
@@ -17,13 +17,15 @@
 var apiUtils = require("../util");
 var express = require("express");
 var runtimeAPI;
+var settings;
 
 module.exports = {
-    init: function(_runtimeAPI) {
+    init: function(_settings, _runtimeAPI) {
         runtimeAPI = _runtimeAPI;
+        settings = _settings;
     },
     app: function() {
-        var app = express();
+        const app = apiUtils.createExpressApp(settings);
 
         // List all SSH keys
         app.get("/", function(req,res) {
diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/theme.js b/packages/node_modules/@node-red/editor-api/lib/editor/theme.js
index c21a7e6e7..b3f6ef922 100644
--- a/packages/node_modules/@node-red/editor-api/lib/editor/theme.js
+++ b/packages/node_modules/@node-red/editor-api/lib/editor/theme.js
@@ -19,6 +19,7 @@ var util = require("util");
 var path = require("path");
 var fs = require("fs");
 var clone = require("clone");
+const apiUtil = require("../util")
 
 var defaultContext = {
     page: {
@@ -40,6 +41,7 @@ var defaultContext = {
         vendorMonaco: ""
     }
 };
+var settings;
 
 var theme = null;
 var themeContext = clone(defaultContext);
@@ -92,7 +94,8 @@ function serveFilesFromTheme(themeValue, themeApp, directory, baseDirectory) {
 }
 
 module.exports = {
-    init: function(settings, _runtimeAPI) {
+    init: function(_settings, _runtimeAPI) {
+        settings = _settings;
         runtimeAPI = _runtimeAPI;
         themeContext = clone(defaultContext);
         if (process.env.NODE_ENV == "development") {
@@ -113,7 +116,15 @@ module.exports = {
         var url;
         themeSettings = {};
 
-        themeApp = express();
+        themeApp = apiUtil.createExpressApp(settings);
+        
+        const defaultServerSettings = {
+            "x-powered-by": false
+        }
+        const serverSettings = Object.assign({},defaultServerSettings,settings.httpServerOptions||{});
+        for (const eOption in serverSettings) {
+            themeApp.set(eOption, serverSettings[eOption]);
+        }
 
         if (theme.page) {
 
diff --git a/packages/node_modules/@node-red/editor-api/lib/index.js b/packages/node_modules/@node-red/editor-api/lib/index.js
index 56f52a222..d9f34eafd 100644
--- a/packages/node_modules/@node-red/editor-api/lib/index.js
+++ b/packages/node_modules/@node-red/editor-api/lib/index.js
@@ -37,7 +37,6 @@ var adminApp;
 var server;
 var editor;
 
-
 /**
  * Initialise the module.
  * @param  {Object}     settings   The runtime settings
@@ -49,7 +48,7 @@ var editor;
 function init(settings,_server,storage,runtimeAPI) {
     server = _server;
     if (settings.httpAdminRoot !== false) {
-        adminApp = express();
+        adminApp = apiUtil.createExpressApp(settings);
 
         var cors = require('cors');
         var corsHandler = cors({
@@ -64,14 +63,6 @@ function init(settings,_server,storage,runtimeAPI) {
             }
         }
 
-        var defaultServerSettings = {
-            "x-powered-by": false
-        }
-        var serverSettings = Object.assign({},defaultServerSettings,settings.httpServerOptions||{});
-        for (var eOption in serverSettings) {
-            adminApp.set(eOption, serverSettings[eOption]);
-        }
-
         auth.init(settings,storage);
 
         var maxApiRequestSize = settings.apiMaxLength || '5mb';
@@ -136,10 +127,11 @@ async function stop() {
         editor.stop();
     }
 }
+
 module.exports = {
-    init: init,
-    start: start,
-    stop: stop,
+    init,
+    start,
+    stop,
 
     /**
     * @memberof @node-red/editor-api
diff --git a/packages/node_modules/@node-red/editor-api/lib/util.js b/packages/node_modules/@node-red/editor-api/lib/util.js
index 621fd9e33..f1420235a 100644
--- a/packages/node_modules/@node-red/editor-api/lib/util.js
+++ b/packages/node_modules/@node-red/editor-api/lib/util.js
@@ -14,10 +14,9 @@
  * limitations under the License.
  **/
 
+const express = require("express");
 
-var log = require("@node-red/util").log; // TODO: separate module
-var i18n = require("@node-red/util").i18n; // TODO: separate module
-
+const { log, i18n } = require("@node-red/util");
 
 module.exports = {
     errorHandler: function(err,req,res,next) {
@@ -64,5 +63,17 @@ module.exports = {
             path: req.path,
             ip: (req.headers && req.headers['x-forwarded-for']) || (req.connection && req.connection.remoteAddress) || undefined
         }
+    },
+    createExpressApp: function(settings) {
+        const app = express();
+    
+        const defaultServerSettings = {
+            "x-powered-by": false
+        }
+        const serverSettings = Object.assign({},defaultServerSettings,settings.httpServerOptions||{});
+        for (let eOption in serverSettings) {
+            app.set(eOption, serverSettings[eOption]);
+        }
+        return app
     }
 }
diff --git a/packages/node_modules/@node-red/runtime/lib/index.js b/packages/node_modules/@node-red/runtime/lib/index.js
index a886cd2ca..74f03c55c 100644
--- a/packages/node_modules/@node-red/runtime/lib/index.js
+++ b/packages/node_modules/@node-red/runtime/lib/index.js
@@ -89,6 +89,15 @@ function init(userSettings,httpServer,_adminApi) {
 
     nodeApp = express();
     adminApp = express();
+    const defaultServerSettings = {
+        "x-powered-by": false
+    }
+    const serverSettings = Object.assign({},defaultServerSettings,userSettings.httpServerOptions||{});
+    for (let eOption in serverSettings) {
+        nodeApp.set(eOption, serverSettings[eOption]);
+        adminApp.set(eOption, serverSettings[eOption]);
+    }
+
 
     if (_adminApi) {
         adminApi = _adminApi;


Test patch:
diff --git a/test/unit/@node-red/editor-api/lib/editor/index_spec.js b/test/unit/@node-red/editor-api/lib/editor/index_spec.js
index 8ed4d88f3..2c11acbc3 100644
--- a/test/unit/@node-red/editor-api/lib/editor/index_spec.js
+++ b/test/unit/@node-red/editor-api/lib/editor/index_spec.js
@@ -61,12 +61,14 @@ describe("api/editor/index", function() {
                 sinon.stub(NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/"+m),"init").callsFake(function(){});
             });
             sinon.stub(NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/theme"),"app").callsFake(function(){ return express()});
+            sinon.stub(NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/settings"),"sshkeys").callsFake(function(){ return express()});
         });
         after(function() {
             mockList.forEach(function(m) {
                 NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/"+m).init.restore();
             })
             NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/theme").app.restore();
+            NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/settings").sshkeys.restore();
             auth.needsPermission.restore();
             log.error.restore();
         });
diff --git a/test/unit/@node-red/editor-api/lib/editor/settings_spec.js b/test/unit/@node-red/editor-api/lib/editor/settings_spec.js
index 171dca564..19f72977a 100644
--- a/test/unit/@node-red/editor-api/lib/editor/settings_spec.js
+++ b/test/unit/@node-red/editor-api/lib/editor/settings_spec.js
@@ -41,7 +41,7 @@ describe("api/editor/settings", function() {
     });
 
     it('returns the user settings', function(done) {
-        info.init({
+        info.init({}, {
             settings: {
                 getUserSettings: function(opts) {
                     if (opts.user !== "fred") {
@@ -67,7 +67,7 @@ describe("api/editor/settings", function() {
     });
     it('updates the user settings', function(done) {
         var update;
-        info.init({
+        info.init({}, {
             settings: {
                 updateUserSettings: function(opts) {
                     if (opts.user !== "fred") {
diff --git a/test/unit/@node-red/editor-api/lib/editor/sshkeys_spec.js b/test/unit/@node-red/editor-api/lib/editor/sshkeys_spec.js
index 1647cd99d..75e8063f6 100644
--- a/test/unit/@node-red/editor-api/lib/editor/sshkeys_spec.js
+++ b/test/unit/@node-red/editor-api/lib/editor/sshkeys_spec.js
@@ -34,7 +34,7 @@ describe("api/editor/sshkeys", function() {
         }
     }
     before(function() {
-        sshkeys.init(mockRuntime);
+        sshkeys.init({}, mockRuntime);
         app = express();
         app.use(bodyParser.json());
         app.use("/settings/user/keys", sshkeys.app());