From e67d49a0f2a553ea1239571fa3ff332910543cf9 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 16:48:58 +0300 Subject: [PATCH] [fix][core] keep the stored member language a language code The member language is stored from /user/settings/lang with no validation, and it reaches file paths in several readers. api/utils/localization.js sanitizes it in both getProperties and getProperty, but the two plugin localization lookups in plugins/plugins/api/api.js concatenate it into a path as it comes: local_path = fullpath + "/frontend/public/localization/" + resultObj.code + "_" + params.member.lang + ".properties"; Validate the value where it is stored, so all readers inherit it, and sanitize the two readers that build a path by hand, the same way localization.js already does. Both changes are cheap and independent, which matters because the value has around 25 readers and only these two had missed the helper. Accepting a language code rather than an allow list from locale.conf.js keeps a deployment that ships its own localization file working, while still leaving no way for path syntax to reach storage. No behaviour change for the dashboard, which only ever sends codes from its own list. --- frontend/express/app.js | 12 ++++++++++-- plugins/plugins/api/api.js | 6 ++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/frontend/express/app.js b/frontend/express/app.js index 2389ebb7f66..142368d7b2f 100644 --- a/frontend/express/app.js +++ b/frontend/express/app.js @@ -75,6 +75,10 @@ var versionInfo = require('./version.info'), { validateCreate } = require('../../api/utils/rights.js'), tracker = require('../../api/parts/mgmt/tracker.js'); +//Language codes as the dashboard uses them, e.g. "en", "pt-br", "zh_CN". Anything +//else is refused rather than sanitized, so a bad value never reaches storage. +var LANG_CODE_RE = /^[A-Za-z]{2,3}(?:[-_][A-Za-z0-9]{2,8})?$/; + console.log("Starting Countly", "version", versionInfo.version, "package", pack.version); var COUNTLY_NAMED_TYPE = "Countly Lite v" + COUNTLY_VERSION; @@ -1849,8 +1853,12 @@ Promise.all([plugins.dbConnection(countlyConfig), plugins.dbConnection("countly_ var updatedUser = {}; - if (req.body.lang) { - updatedUser.lang = req.body.lang; + //The stored lang reaches file paths in several places, for example + //api/utils/localization.js and the plugin localization lookups, so keep it to a + //language code here rather than relying on every reader to sanitize it. The + //dashboard only ever sends codes from its own localization list. + if (req.body.lang && LANG_CODE_RE.test(req.body.lang + "")) { + updatedUser.lang = req.body.lang + ""; countlyDb.collection('members').update({"_id": countlyDb.ObjectID(req.session.uid + "")}, {'$set': updatedUser}, {safe: true}, function(err, member) { if (member && !err) { diff --git a/plugins/plugins/api/api.js b/plugins/plugins/api/api.js index 02165726e8a..90fb7095094 100644 --- a/plugins/plugins/api/api.js +++ b/plugins/plugins/api/api.js @@ -152,7 +152,8 @@ var plugin = {}, if (!resultObj.enabled) { var local_path = fullpath + "/frontend/public/localization/" + resultObj.code + ".properties"; if (params.member.lang && params.member.lang !== "en") { - local_path = fullpath + "/frontend/public/localization/" + resultObj.code + "_" + params.member.lang + ".properties"; + //sanitized the same way api/utils/localization.js does it + local_path = fullpath + "/frontend/public/localization/" + resultObj.code + "_" + common.sanitizeFilename(params.member.lang) + ".properties"; } if (fs.existsSync(local_path)) { var local_properties = fs.readFileSync(local_path); @@ -379,7 +380,8 @@ var plugin = {}, var fullpath = path.resolve(__dirname, "../"); var local_path = fullpath + "/frontend/public/localization/plugins.properties"; if (params.member.lang && params.member.lang !== "en") { - path = fullpath + "/frontend/public/localization/plugins" + "_" + params.member.lang + ".properties"; + //sanitized the same way api/utils/localization.js does it + path = fullpath + "/frontend/public/localization/plugins" + "_" + common.sanitizeFilename(params.member.lang) + ".properties"; if (fs.existsSync(path)) { local_path = path; }