diff --git a/.github/workflows/release_notice.yml b/.github/workflows/release_notice.yml index 8a7a8431ee0..abe4dfd2eba 100644 --- a/.github/workflows/release_notice.yml +++ b/.github/workflows/release_notice.yml @@ -14,7 +14,7 @@ jobs: run: echo "$GITHUB_CONTEXT" - name: Send custom JSON data to Slack workflow id: slack - uses: slackapi/slack-github-action@v3.0.2 + uses: slackapi/slack-github-action@v3.0.3 with: # This data can be any valid JSON from a previous step in the GitHub Action webhook: ${{ secrets.SLACK_RELEASE }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 49f40e4f229..ffa81cec811 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,65 @@ + ## Version 25.03.X +Fixes: +- [core] Accept numeric color in saveNote schema so graph note create/edit no longer fails validation +- [groups] Tolerate legacy string `group_id` on members in findGroups aggregation so the groups listing, User Management, Alerts and Preset Management pages no longer 400 with MongoDB Location40081 on tenants with pre-2021 data + +## Version 25.03.44 +Security fixes: +- [alerts] Validate alertConfig.selectedApps against caller's permissions (cross-app metric exfiltration) +- [app_users / logger / compliance-hub] Strip dangerous Mongo operators ($where, $expr, $function, $accumulator) from user-supplied queries +- [app_users] Sanitize user.picture filename before deletion (path traversal) +- [app_users] Scope export download/delete to caller's app_id; reject path-traversal in filenames +- [apps] Replace updateApp/createApp mass-assignment with explicit field allowlist +- [auth] Generate new-member invite prid with crypto.randomBytes (replace predictable HMAC) +- [auth] Handle req.session.regenerate error in token login +- [auth] Replace OTP-equality recaptcha bypass with twoFactorPassed session flag +- [auth] Restrict /login/token/:token to login-purpose tokens; regenerate session id on token login to close fixation +- [cms / system / systemlogs] /i/cms/save_entries, /o/system/plugins, /i/systemlogs restricted to global admins +- [core] Add common.resolvePathInBase helper for safe path containment checks +- [crashes] Add error handlers to crash report streamed responses +- [dashboards] Constrain public screenshot route paths and stream error handling +- [dashboards] Identical response for missing/inaccessible dashboard (no enumeration) +- [dashboards] Require auth + per-widget app permission on /o/dashboards/test; remove the unused endpoint +- [data_migration] Constrain export/import paths to allowed directories; reject path-traversal in target_path, multipart filenames, and exportid (backport of #7491) +- [data] Escape regex metacharacters in sSearch parameters (ReDoS) +- [data] Return 404 (not 500) when event_groups lookup misses +- [dbviewer] Block $graphLookup aggregation stage (cross-collection data exfiltration) +- [dbviewer] Wrap non-admin scope as top-level $and so user-supplied $or/$nor cannot bypass per-tenant filter (cross-tenant data exfiltration) +- [errorlogs] Reject path-traversal in admin log file paths +- [event_groups] Whitelist updatable fields on create/update; scope reads by app_id +- [exports] Add stream error handlers to export download +- [exports] Authorize /o/export/download by task ownership / app_id +- [notes] Bind notes to permission-checked app_id; check edit permissions against the note's stored app_id +- [notes] Enforce saveNote schema validation +- [output] Remove noescape query-string bypass on returnOutput (reflected-XSS via parameter) +- [push] Bind message create/test/update/one/remove/toggle to query-string app_id (cross-app push injection) +- [redirect] Apply SSRF protection (api/utils/ssrf-protection.js) to app.redirect_url outbound requests +- [render] (--disable-web-security) removed from puppeteer +- [reports] Add stream error handlers +- [star-rating] Close stored XSS in feedback widget logo upload/preview; restrict uploads to image MIME types and validate magic bytes (backport of #7532) +- [star-rating] Defense-in-depth on image upload/serve routes +- [system-utility] Harden streamed responses with error handlers +- [tasks] Authorize /i/tasks/{update,delete,name,edit} per task ownership / app admin / global admin +- [users] /users/check/username now requires global admin (parity with email check) + +Enterprise Features: +- [journey_engine] Maker checker approver +- [journey_engine] Engagement cooldown information added to journey builder and user profiles + Enterprise Fixes: +- [active_users] Fixed logic to prevent triggering active users calculation if it +- [cognito] Fix crash on GET /clogin/:code when body-parser 2.x leaves req.body undefined on requests with no bodyis already running. - [drill] Add query hint based on default indexes - [drill] Add contextual links in drill table for user IDs and crash groups - [drill] Resolve device IDs to user profiles via server-side redirect endpoint - [drill] Open crash group and user profile links in new tab - [drill] Show user-friendly error message when saving a query fails - +- [users] Fix MongoDB dot encoding (.) leaking into user profile UI filters, breakdown dropdown, and URLs ## Version 25.03.43 Enterprise Fixes: - [flow] Optimize timeline period query -- [journey_engine] Maker checker approver Dependencies: - Bump follow-redirects from 1.15.11 to 1.16.0 diff --git a/api/parts/mgmt/users.js b/api/parts/mgmt/users.js index 75348a28703..9560efa9d0b 100644 --- a/api/parts/mgmt/users.js +++ b/api/parts/mgmt/users.js @@ -904,15 +904,18 @@ usersApi.saveNote = async function(params) { }, 'ts': { 'required': true, - 'type': '' + 'type': 'IntegerString' }, 'noteType': { 'required': true, 'type': 'String', }, 'color': { + // Frontend (countly.common.notes.js COLOR_TAGS) sends a numeric + // index 1..5. URL query callers may send "5" as a string. + // Mirror the ts handling — IntegerString accepts both. 'required': true, - 'type': 'String' + 'type': 'IntegerString' }, 'category': { 'required': false, @@ -1187,4 +1190,4 @@ usersApi.ackNotification = function(params) { }); }; -module.exports = usersApi; \ No newline at end of file +module.exports = usersApi; diff --git a/api/utils/common.js b/api/utils/common.js index 4402b843310..be62bfcd055 100644 --- a/api/utils/common.js +++ b/api/utils/common.js @@ -1403,7 +1403,10 @@ common.returnMessage = function(params, returnCode, message, heads, noResult = f else { console.error("Output already closed, can't write more"); console.trace(); - console.log(params); + // Don't dump the full params object — req.body/req.headers can + // contain credentials, session cookies, or other secrets. Log + // only the pathname (query string can carry api_key/auth_token). + console.log({pathname: params.urlParts && params.urlParts.pathname, apiPath: params.apiPath, qstringKeys: params.qstring && Object.keys(params.qstring)}); } } }; @@ -1485,7 +1488,10 @@ common.returnOutput = function(params, output, noescape, heads) { else { console.error("Output already closed, can't write more"); console.trace(); - console.log(params); + // Don't dump the full params object — req.body/req.headers can + // contain credentials, session cookies, or other secrets. Log + // only the pathname (query string can carry api_key/auth_token). + console.log({pathname: params.urlParts && params.urlParts.pathname, apiPath: params.apiPath, qstringKeys: params.qstring && Object.keys(params.qstring)}); } } }; diff --git a/frontend/express/app.js b/frontend/express/app.js index ab7a1c5b0a6..47799765af3 100644 --- a/frontend/express/app.js +++ b/frontend/express/app.js @@ -479,14 +479,16 @@ Promise.all([plugins.dbConnection(countlyConfig), plugins.dbConnection("countly_ app.use(cookieParser()); //server theme images app.use(function(req, res, next) { - var urlPath = req.url.replace(countlyConfig.path, ""); + var urlPath = req.path.replace(countlyConfig.path, ""); var theme = req.cookies.theme || curTheme; - if (theme && theme.length && (req.url.indexOf(countlyConfig.path + '/images/') === 0 || req.url.indexOf(countlyConfig.path + '/geodata/') === 0)) { - fs.exists(__dirname + '/public/themes/' + theme + urlPath, function(exists) { - if (exists) { - res.sendFile(__dirname + '/public/themes/' + theme + urlPath); - } - else { + if (theme && theme.length && (req.path.indexOf(countlyConfig.path + '/images/') === 0 || req.path.indexOf(countlyConfig.path + '/geodata/') === 0)) { + // Both `theme` (cookie) and `urlPath` (URL) are user-controlled. + // Hand the relative path to res.sendFile with `root` set to + // /public/themes — express normalizes the path and rejects any + // `..` traversal before touching the filesystem. Missing files + // surface via the error callback and fall through to next(). + res.sendFile(theme + urlPath, {root: path.resolve(__dirname, 'public/themes')}, function(err) { + if (err) { next(); } }); diff --git a/frontend/express/public/javascripts/countly/vue/components/content.js b/frontend/express/public/javascripts/countly/vue/components/content.js index 6a35382158a..e4e84018130 100644 --- a/frontend/express/public/javascripts/countly/vue/components/content.js +++ b/frontend/express/public/javascripts/countly/vue/components/content.js @@ -71,6 +71,11 @@ type: String }, + cooldownBadge: { + default: () => ({ show: false }), + type: Object + }, + status: { default: () => ({ label: 'Status', diff --git a/frontend/express/public/javascripts/countly/vue/templates/content/content-header.html b/frontend/express/public/javascripts/countly/vue/templates/content/content-header.html index e5f12fa4359..41f9f909e73 100644 --- a/frontend/express/public/javascripts/countly/vue/templates/content/content-header.html +++ b/frontend/express/public/javascripts/countly/vue/templates/content/content-header.html @@ -55,6 +55,18 @@ :tabs="tabs" />