Security Guidelines
January 14, 2026 · View on GitHub
This document outlines security requirements and best practices for Countly Server development. All contributors must follow these guidelines to ensure the security of the platform.
Table of Contents
- API Endpoint Security
- Cross-App Operation Security
- XSS Prevention
- MongoDB Injection Prevention
- File Upload Security
- Command Line Security
- CSV Injection Prevention
- Brute Force Prevention
API Endpoint Security
All API endpoints (except special cases) must be secured using validation methods from api/utils/rights.js.
Available Validation Methods
| Method | Purpose | Required Params |
|---|---|---|
validateUser | Verify user exists | api_key or auth_token |
validateRead | Check read permission on feature | api_key, app_id |
validateCreate | Check create permission on feature | api_key, app_id |
validateUpdate | Check update permission on feature | api_key, app_id |
validateDelete | Check delete permission on feature | api_key, app_id |
validateGlobalAdmin | Check global admin status | api_key |
dbUserHasAccessToCollection | Check collection-level access | member, app_id |
Usage Examples
const { validateRead, validateUpdate, validateDelete, validateGlobalAdmin } = require('../../../api/utils/rights.js');
// Read permission check
plugins.register("/o/myfeature", function(ob) {
var params = ob.params;
validateRead(params, 'myfeature', function() {
// User has read access to this feature for the app
common.returnOutput(params, data);
});
});
// Write permission check
plugins.register("/i/myfeature/update", function(ob) {
var params = ob.params;
validateUpdate(params, 'myfeature', function() {
// User has update permission
performUpdate(params);
});
});
// Global admin check (no app context)
plugins.register("/i/admin/settings", function(ob) {
var params = ob.params;
validateGlobalAdmin(params, function() {
// User is a global administrator
updateGlobalSettings(params);
});
});
Collection-Level Access
For endpoints that expose entire collections (like data export):
const { dbUserHasAccessToCollection } = require('../../../api/utils/rights.js');
validateRead(params, 'core', function() {
dbUserHasAccessToCollection(params, params.qstring.collection, function(hasAccess) {
if (hasAccess) {
exportData(params);
} else {
common.returnMessage(params, 401, 'User does not have access to this collection');
}
});
});
Cross-App Operation Security
Critical: All edit/delete operations must verify the resource belongs to the authorized app.
❌ Insecure Pattern
// DANGEROUS: User can manipulate resources from other apps
validateDelete(params, 'cohorts', function() {
db.collection("cohorts").deleteOne({_id: params.qstring.id});
});
An attacker could:
- Get delete permission for App A
- Provide a cohort ID from App B
- Delete App B's cohort without authorization
✅ Secure Pattern
// SAFE: Verify resource belongs to authorized app
validateDelete(params, 'cohorts', function() {
db.collection("cohorts").deleteOne({
_id: params.qstring.id,
app_id: params.app_id + "" // Cast to string for consistency
});
});
This applies to all operations:
deleteOne/deleteManyupdateOne/updateManyfindOneAndUpdate/findOneAndDelete
XSS Prevention
Backend (API)
API responses are automatically escaped when using standard output methods:
// ✅ Auto-escaped - safe to use
common.returnOutput(params, data);
common.returnMessage(params, 200, 'Success');
For custom output, manually escape these characters:
"→"&→&'→'<→<>→>
Use the built-in escape function:
var safeString = common.escape_html(unsafeString);
Frontend
Data from API should be rendered as text (Vue will escape it):
<!-- ✅ Correct: Render API-provided text safely -->
<p>{{ apiData.description }}</p>
User input that bypasses API must be escaped as text:
<!-- ✅ Safe: Vue automatically escapes in text interpolation -->
<span>{{ userInput }}</span>
Never use v-html with raw user input:
<!-- ❌ DANGEROUS: XSS vulnerability -->
<div v-html="userProvidedContent"></div>
Manual Sanitization
// Frontend sanitization
var sanitized = countlyCommon.encodeHtml(userInput);
// Test inputs to verify XSS protection
var testString = "<script>'&&&'</script>";
// Should display exactly as: <script>'&&&'</script>
MongoDB Injection Prevention
MongoDB operations using the official driver are generally safe from code injection. However, data manipulation attacks are still possible.
The Vulnerability
// User submits: {"username": "admin", "password": {"$ne": 1}}
var params = {
username: "admin",
password: {"$ne": 1} // Matches any password not equal to 1
};
db.collection("members").findOne(params, function(err, user) {
if (!err && user) {
// ATTACKER AUTHENTICATED without knowing password!
}
});
Prevention
Always cast authentication credentials to strings:
// ✅ Safe: Force string type
params.username = params.username + "";
params.password = params.password + "";
db.collection("members").findOne({
username: params.username,
password: params.password
}, function(err, user) {
// Now safe from object injection
});
For objects, validate no MongoDB operators are present:
function isSafeQuery(obj) {
for (var key in obj) {
if (key.startsWith('$')) {
return false; // MongoDB operator detected
}
if (typeof obj[key] === 'object' && obj[key] !== null && !isSafeQuery(obj[key])) {
return false;
}
}
return true;
}
File Upload Security
Validate File Types
var tmp_path = params.files.upload.path;
var type = params.files.upload.type;
// Whitelist allowed types
var allowedTypes = ["image/png", "image/gif", "image/jpeg"];
if (!allowedTypes.includes(type)) {
// Delete the uploaded file
fs.unlink(tmp_path, function() {});
common.returnMessage(params, 400, 'Invalid file type');
return;
}
// Additional: Verify file magic bytes match claimed type
// (type header can be spoofed)
Sanitize Filenames
Never use user-provided filenames directly:
// ❌ Dangerous: Path traversal possible
// User provides: "../../../etc/passwd"
var filename = params.qstring.filename;
fs.writeFile('/uploads/' + filename, data);
// ✅ Safe: Sanitize filename
var safeFileName = common.sanitizeFilename(params.qstring.filename);
fs.writeFile('/uploads/' + safeFileName, data);
The sanitizeFilename function:
- Removes path separators (
/,\) - Removes null bytes
- Limits length
- Removes dangerous characters
Command Line Security
The Vulnerability
var exec = require('child_process').exec;
// ❌ DANGEROUS: Command injection
var scriptPath = userInput; // User provides: "myscript.js; rm -rf /"
exec("nodejs " + scriptPath, callback);
// Executes: nodejs myscript.js; rm -rf /
Prevention
Use spawn with argument arrays:
var cp = require('child_process');
// ✅ Safe: Arguments are properly escaped
var scriptPath = userInput; // Even if: "myscript.js; rm -rf /"
var process = cp.spawn("nodejs", [scriptPath]);
process.on('close', function(code) {
console.log('Exited with code:', code);
});
// The malicious input is treated as a literal filename
// nodejs will fail to find file named "myscript.js; rm -rf /"
If you must use exec, sanitize rigorously:
var shellEscape = require('shell-escape');
var safeArgs = shellEscape([userInput]);
exec("nodejs " + safeArgs, callback);
CSV Injection Prevention
When exporting data to CSV or Excel, cell values starting with special characters can be interpreted as formulas.
The Attack
A malicious user stores data like:
=cmd|' /C calc'!A0
When exported to CSV and opened in Excel, this launches the calculator (or worse).
Prevention
var exports = require('../../../api/parts/data/exports.js');
// Use the built-in function
var safeValue = exports.preventCSVInjection(cellValue);
// Or manually prefix dangerous characters
function preventCSVInjection(value) {
if (typeof value === 'string') {
var dangerous = ['=', '+', '-', '@', '\t', '\r'];
if (dangerous.includes(value.charAt(0))) {
return "'" + value; // Prefix with single quote
}
}
return value;
}
Brute Force Prevention
Protect authentication endpoints from brute force attacks.
var preventBruteforce = require('../../../frontend/express/libs/preventBruteforce.js');
function login(req, res) {
var username = req.body.username;
var password = req.body.password;
preventBruteforce.isBlocked("login", username, function(isBlocked, fails, err) {
if (isBlocked) {
res.status(429).json({error: "Too many failed attempts. Please try again later."});
return;
}
authenticateUser(username, password, function(success) {
if (success) {
// Reset fail counter on successful login
preventBruteforce.reset("login", username);
// ... complete login
} else {
// Increment fail counter
preventBruteforce.fail("login", username);
res.status(401).json({error: "Invalid credentials"});
}
});
});
}
Rate Limiting
API endpoints can use rate limiting:
const { RateLimiterMemory } = require("rate-limiter-flexible");
const rateLimiter = new RateLimiterMemory({
points: 10, // 10 requests
duration: 1, // per 1 second
});
async function handleRequest(params) {
try {
await rateLimiter.consume(params.ip_address);
// Process request
} catch (rejRes) {
common.returnMessage(params, 429, "Too Many Requests");
}
}
Security Checklist
Before submitting code, verify:
- All API endpoints use appropriate validation methods
- All database operations include
app_idwhere applicable - User credentials are cast to strings
- File uploads validate type and sanitize filename
- Command line arguments use
spawnwith arrays - CSV exports use injection prevention
- Authentication endpoints have brute force protection
- No
v-htmlwith unsanitized user input - Sensitive data is not logged
Reporting Security Issues
If you discover a security vulnerability:
- DO NOT create a public GitHub issue
- Email security@count.ly with details
- Include steps to reproduce
- Allow time for a fix before disclosure
See SECURITY.md in the repository root for more information.