Skip to content

Commit f58e053

Browse files
author
Cihad Tekin
committed
Merge branch 'master' into SER-2754-2-fa-remove-2-fa-secret-from-dashboard
2 parents e31430f + 9e76bb4 commit f58e053

7 files changed

Lines changed: 84 additions & 17 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ Fixes:
33
- [core] fixes for changeOwner script
44
- [core] Add null checking for user permission when opening the dashboard
55
- [core] Preserve URL hash during oauth
6+
- [core] Rate limiting for api endpoints
67
- [2fa] Removed the secret and qr code from the dashboard response
78

89
Enterprise Fixes:
@@ -30,7 +31,7 @@ Dependencies:
3031
- Bump sass from 1.93.3 to 1.96.0
3132
- Bump sass-embedded from 1.93.3 to 1.96.0
3233
- Bump sharp from 0.34.4 to 0.34.5
33-
- Bump sharp from 0.34.4 to 0.34.5
34+
- Bump sharp from 0.34.4 to 0.34.5
3435
- Bump swiper from 11.2.10 to 12.0.3
3536
- Bump terser from 5.44.0 to 5.44.1
3637
- Bump vite from 7.1.12 to 7.2.7

api/api.js

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const pack = require('../package.json');
1717
const versionInfo = require('../frontend/express/version.info.js');
1818
const moment = require("moment");
1919
const tracker = require('./parts/mgmt/tracker.js');
20+
const { RateLimiterMemory } = require("rate-limiter-flexible");
2021

2122
var t = ["countly:", "api"];
2223
common.processRequest = processRequest;
@@ -119,6 +120,8 @@ plugins.connectToAllDatabases().then(function() {
119120
api_additional_headers: "X-Frame-Options:deny\nX-XSS-Protection:1; mode=block\nStrict-Transport-Security:max-age=31536000; includeSubDomains; preload\nAccess-Control-Allow-Origin:*",
120121
dashboard_rate_limit_window: 60,
121122
dashboard_rate_limit_requests: 500,
123+
api_rate_limit_window: 0,
124+
api_rate_limit_requests: 0,
122125
proxy_hostname: "",
123126
proxy_port: "",
124127
proxy_username: "",
@@ -375,6 +378,35 @@ plugins.connectToAllDatabases().then(function() {
375378
console.log("Starting worker", process.pid, "parent:", process.ppid);
376379
const taskManager = require('./utils/taskmanager.js');
377380

381+
const rateLimitWindow = parseInt(plugins.getConfig("security").api_rate_limit_window, 10) || 0;
382+
const rateLimitRequests = parseInt(plugins.getConfig("security").api_rate_limit_requests, 10) || 0;
383+
const rateLimiterInstance = new RateLimiterMemory({ points: rateLimitRequests, duration: rateLimitWindow });
384+
const requiresRateLimiting = rateLimitWindow > 0 && rateLimitRequests > 0;
385+
const omit = /^\/i(\/bulk)?(\?|$)/; // omit /i endpoint from rate limiting
386+
/**
387+
* Rate Limiting Middleware
388+
* @param {Function} next - The next middleware function
389+
* @returns {Function} - The wrapped middleware function with rate limiting
390+
*/
391+
const rateLimit = (next) => {
392+
if (!requiresRateLimiting) {
393+
return next;
394+
}
395+
return (req, res) => {
396+
if (omit.test(req.url)) {
397+
return next(req, res);
398+
}
399+
const ip = common.getIpAddress(req);
400+
rateLimiterInstance
401+
.consume(ip)
402+
.then(() => next(req, res))
403+
.catch(() => {
404+
log.w(`Rate limit exceeded for IP: ${ip}`);
405+
common.returnMessage({ req, res, qstring: {} }, 429, "Too Many Requests");
406+
});
407+
};
408+
};
409+
378410
common.cache = new CacheWorker();
379411
common.cache.start();
380412

@@ -412,10 +444,10 @@ plugins.connectToAllDatabases().then(function() {
412444
if (common.config.api.ssl.ca) {
413445
sslOptions.ca = fs.readFileSync(common.config.api.ssl.ca);
414446
}
415-
server = https.createServer(sslOptions, handleRequest);
447+
server = https.createServer(sslOptions, rateLimit(handleRequest));
416448
}
417449
else {
418-
server = http.createServer(handleRequest);
450+
server = http.createServer(rateLimit(handleRequest));
419451
}
420452

421453
server.listen(serverOptions.port, serverOptions.host).timeout = common.config.api.timeout || 120000;

api/utils/common.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1486,7 +1486,9 @@ var ipLogger = common.log('ip:api');
14861486
common.getIpAddress = function(req) {
14871487
var ipAddress = "";
14881488
if (req) {
1489-
if (req.headers) {
1489+
// TODO: add config option to trust x-forwarded-for header
1490+
// or add a configuration option to set trusted proxies
1491+
if (req.headers && ("x-forwarded-for" in req.headers || "x-real-ip" in req.headers)) {
14901492
ipAddress = req.headers['x-forwarded-for'] || req.headers['x-real-ip'] || "";
14911493
}
14921494
else if (req.connection && req.connection.remoteAddress) {

frontend/express/views/dashboard.html

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2005,7 +2005,36 @@ <h4><a href="#/analytics/events/key/{{encodeURIComponent this.name}}">{{this.nam
20052005
if (countlyGlobal.tracking.server_feedback) {
20062006
//display in app messages
20072007
if (Countly.content && Countly.content.enterContentZone) {
2008-
Countly.content.enterContentZone();
2008+
Countly.user_details({custom:{content_messages:true}}); // Ensure user is eligible for content messages
2009+
Countly.content.enterContentZone(function(params){
2010+
if (params.journeyId && (params.widget_id || params.id)) {
2011+
try {
2012+
var check = params.journeyId + "_";
2013+
if (params.widget_id) {
2014+
check += "widget_" + params.widget_id;
2015+
}
2016+
else {
2017+
check += "content_" + params.id;
2018+
}
2019+
var key = "cly_seenJourneyIds_c2VlbkpvdXJuZXlJZHM"; // Base64 for 'seenJourneyIds' to avoid potential key conflicts
2020+
var seen = JSON.parse(localStorage.getItem(key) || "[]");
2021+
2022+
if (seen.includes(check)) {
2023+
return false;
2024+
}
2025+
2026+
seen.push(check);
2027+
localStorage.setItem(key, JSON.stringify(seen));
2028+
return true;
2029+
}
2030+
catch (e) {
2031+
// Handle localStorage errors (disabled, full, security exceptions in private browsing)
2032+
// Fail open - allow content to be shown if we can't track it
2033+
return false;
2034+
}
2035+
}
2036+
return false;
2037+
});
20092038
}
20102039
}
20112040
}

package-lock.json

Lines changed: 10 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
"offline-geocoder": "git+https://github.com/Countly/offline-geocoder.git",
9393
"properties-parser": "0.6.0",
9494
"puppeteer": "^24.6.1",
95+
"rate-limiter-flexible": "^9.0.1",
9596
"sass": "1.96.0",
9697
"semver": "^7.7.1",
9798
"sharp": "^0.34.2",

plugins/plugins/frontend/public/localization/plugins.properties

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ configs.user-level-configuration = User Level Configuration
127127
configs.table-description = Settings in this section will override global settings for the user
128128
configs.security-dashboard_rate_limit_window = Dashboard Rate Limit Time (seconds)
129129
configs.security-dashboard_rate_limit_requests = Dashboard Request Rate Limit
130+
configs.security-api_rate_limit_window = API Rate Limit Time (seconds)
131+
configs.security-api_rate_limit_requests = API Request Rate Limit
130132
configs.danger-zone = Danger Zone
131133
configs.password = Password
132134
configs.fill-required-fields = Please fill all required fields
@@ -234,6 +236,8 @@ configs.help.security-password_expiration = Number of days after which user must
234236
configs.help.user-level-configuration = Allow separate dashboard users to change these configs for their account only.
235237
configs.help.security-dashboard_rate_limit_window = Will start blocking if request amount is reached in this time window
236238
configs.help.security-dashboard_rate_limit_requests = How many requests to allow per time window?
239+
configs.help.security-api_rate_limit_window = Will start blocking if request amount is reached in this time window. Requires a server restart.
240+
configs.help.security-api_rate_limit_requests = How many requests to allow per time window? Requires a server restart.
237241
configs.help.push-proxyhost = Hostname or IP address of HTTP CONNECT proxy server to use for communication with APN & FCM when sending push notifications.
238242
configs.help.push-proxyport = Port number of the proxy server
239243
configs.help.push-proxyuser = (if needed) Username for proxy server HTTP Basic authentication

0 commit comments

Comments
 (0)