This document outlines the security considerations, known issues, and best practices for using the Feathers Elasticsearch adapter in production environments.
Last Security Review: 2025-11-03
Last Security Update: 2025-11-03
Overall Risk Level: LOW (after v4.0.0 security improvements)
Production Ready: Yes
The following security improvements have been implemented in version 4.0.0:
- What: Prevents stack overflow attacks via deeply nested queries
- Default: Maximum depth of 50 levels
- Configuration:
security.maxQueryDepth - Impact: Blocks malicious queries like
{ $or: [{ $or: [...] }] }nested 1000+ levels deep
- What: Prevents DoS via mass update/delete operations
- Default: Maximum 10,000 documents per bulk operation
- Configuration:
security.maxBulkOperations - Impact: Prevents accidental or malicious operations affecting millions of documents
- What: Restricts which Elasticsearch API methods can be called via
raw() - Default: All methods disabled (empty whitelist)
- Configuration:
security.allowedRawMethods - Impact: BREAKING CHANGE - Must explicitly enable raw methods needed
- What: Prevents regex DoS attacks in
$sqs(simple query string) operator - Default: Validates against catastrophic backtracking patterns, 500 char limit
- Configuration:
security.maxQueryStringLength - Impact: Blocks patterns like
/.*.*.*.*that cause CPU exhaustion
- What: Centralized security settings with sensible defaults
- Access: Via
service.securityproperty - Configuration: Pass
securityobject in service options
Configure security settings when creating the service:
import { Client } from '@elastic/elasticsearch';
import service from 'feathers-elasticsearch';
const client = new Client({ node: 'http://localhost:9200' });
app.use('/my-service', service({
Model: client,
index: 'my-index',
// Security configuration
security: {
// Query complexity limits
maxQueryDepth: 50, // Max nesting for $or/$and/$nested (default: 50)
maxArraySize: 10000, // Max items in $in/$nin arrays (default: 10000)
// Bulk operation limits
maxBulkOperations: 10000, // Max documents in bulk patch/remove (default: 10000)
// Document size limits
maxDocumentSize: 10485760, // 10MB max document size (default: 10MB)
// Query string limits for $sqs
maxQueryStringLength: 500, // Max length of $sqs queries (default: 500)
// Raw method whitelist (IMPORTANT: empty by default = all disabled)
allowedRawMethods: [
'search', // Allow search operations
'count', // Allow count operations
// 'indices.delete', // DON'T enable destructive operations!
],
// Cross-index query restrictions
allowedIndices: [], // Empty = only service's index allowed
// Or specify: ['index1', 'index2']
// Field restrictions for $sqs queries
searchableFields: [], // Empty = all fields searchable
// Or specify: ['name', 'email', 'bio']
// Error verbosity
enableDetailedErrors: false, // true in dev, false in production
// Input sanitization
enableInputSanitization: true, // Prevent prototype pollution
}
}));If you don't provide a security configuration, these defaults are used:
{
maxQueryDepth: 50,
maxArraySize: 10000,
maxBulkOperations: 10000,
maxDocumentSize: 10485760, // 10MB
maxQueryStringLength: 500,
allowedRawMethods: [], // β οΈ ALL RAW METHODS DISABLED
allowedIndices: [], // Only default index allowed
searchableFields: [], // All fields searchable
enableDetailedErrors: process.env.NODE_ENV !== 'production',
enableInputSanitization: true
}A comprehensive security review identified no critical vulnerabilities. The high-severity issues found have been addressed in v4.0.0.
- β Query depth validation - RESOLVED
- β Bulk operation limits - RESOLVED
- β Raw method whitelist - RESOLVED
- β Query string sanitization - RESOLVED
- β TypeScript strict mode enabled - Excellent type safety
- β No code injection vulnerabilities - No use of eval(), new Function(), etc.
- β Strong input validation patterns - Consistent use of validateType()
β οΈ Information disclosure - Error messages detailed in dev mode (by design)- βΉοΈ Index name validation - Optional, configure via
security.allowedIndices
Status: β
RESOLVED in v4.0.0
Severity: HIGH
Component: raw() method
Description:
The raw() method allows arbitrary Elasticsearch API calls without authentication, authorization, or input validation. This can be exploited to delete indices, modify cluster settings, or access unauthorized data.
Resolution:
As of v4.0.0, the raw() method is disabled by default. All raw methods are blocked unless explicitly whitelisted via security.allowedRawMethods.
Migration Guide:
If your application uses raw(), you must whitelist the methods:
// v3.x - raw() was unrestricted
app.use('/elasticsearch', service({
Model: client,
// ... other options
}));
// v4.0+ - Must whitelist methods
app.use('/elasticsearch', service({
Model: client,
security: {
allowedRawMethods: ['search', 'count'] // Only allow safe read operations
}
}));
app.service('elasticsearch').hooks({
before: {
raw: [disallow('external')] // Block from external clients
}
});Option B - Implement strict whitelist:
const ALLOWED_RAW_METHODS = new Set(['search', 'count', 'explain']);
app.service('elasticsearch').hooks({
before: {
raw: [
context => {
const method = context.arguments[0];
if (!ALLOWED_RAW_METHODS.has(method)) {
throw new errors.MethodNotAllowed(`Method '${method}' is not allowed`);
}
}
]
}
});Status: Known Issue
Severity: HIGH
Component: $sqs (simple query string) operator
Description:
The $sqs operator accepts user-controlled query strings passed directly to Elasticsearch without sanitization, potentially allowing query injection attacks or regex DoS.
Mitigation:
// Add validation hook
app.service('elasticsearch').hooks({
before: {
find: [
context => {
const { query } = context.params;
if (query && query.$sqs) {
// Validate query string length
if (query.$sqs.$query.length > 500) {
throw new errors.BadRequest('Query string too long');
}
// Prevent regex patterns that could cause catastrophic backtracking
if (/\/\.\*(\.\*)+/.test(query.$sqs.$query)) {
throw new errors.BadRequest('Invalid query pattern');
}
// Whitelist allowed fields
const allowedFields = ['name', 'description', 'tags'];
const requestedFields = query.$sqs.$fields || [];
for (const field of requestedFields) {
const cleanField = field.replace(/\^.*$/, '');
if (!allowedFields.includes(cleanField)) {
throw new errors.BadRequest(`Field '${field}' is not searchable`);
}
}
}
}
]
}
});Status: Known Issue
Severity: HIGH
Components: Bulk patch, bulk remove, complex queries
Description:
Several operations lack safeguards against resource exhaustion:
- No maximum limit on bulk operations (could patch/remove millions of documents)
- No query timeout enforcement
- No validation on deeply nested queries
Mitigation:
app.service('elasticsearch').hooks({
before: {
find: [
// Limit query complexity
context => {
const depth = getQueryDepth(context.params.query);
if (depth > 50) {
throw new errors.BadRequest('Query too complex');
}
}
],
patch: [
// Restrict bulk patches
async context => {
if (context.id === null) {
// This is a bulk operation - check how many documents would be affected
const count = await context.service.find({
...context.params,
paginate: false,
query: { ...context.params.query, $limit: 0 }
});
const maxBulk = 1000;
if (count.total > maxBulk) {
throw new errors.BadRequest(
`Bulk operation would affect ${count.total} documents, maximum is ${maxBulk}`
);
}
}
}
],
remove: [
// Restrict bulk deletes (or disable entirely)
context => {
if (context.id === null) {
throw new errors.MethodNotAllowed('Bulk deletes not allowed');
}
}
]
}
});
// Helper function to calculate query depth
function getQueryDepth(query, depth = 0) {
if (!query || typeof query !== 'object') return depth;
let maxDepth = depth;
for (const key of Object.keys(query)) {
if (key === '$or' || key === '$and') {
const value = query[key];
if (Array.isArray(value)) {
for (const item of value) {
maxDepth = Math.max(maxDepth, getQueryDepth(item, depth + 1));
}
}
}
}
return maxDepth;
}Status: Known Issue
Severity: MEDIUM
Component: Error handler
Description:
Detailed Elasticsearch error information is returned to clients, potentially exposing internal system details like index structure, field names, and cluster configuration.
Mitigation:
app.service('elasticsearch').hooks({
error: {
all: [
context => {
if (process.env.NODE_ENV === 'production') {
// Log full error server-side
console.error('Elasticsearch error:', context.error);
// Return generic message to client
if (context.error.details) {
delete context.error.details;
}
if (context.error.stack) {
delete context.error.stack;
}
// Use generic messages
const genericMessages = {
400: 'Invalid request parameters',
404: 'Resource not found',
409: 'Resource conflict',
500: 'Internal server error'
};
const status = context.error.code || 500;
context.error.message = genericMessages[status] || genericMessages[500];
}
}
]
}
});Status: Known Issue
Severity: MEDIUM
Component: $index filter
Description:
The $index filter allows users to specify arbitrary index names without validation, potentially enabling cross-index data access.
Mitigation:
// Option A - Disable $index filter entirely (recommended)
app.use('/elasticsearch', service({
Model: client,
index: 'my-index',
filters: {
$index: undefined // Remove $index filter
}
}));
// Option B - Implement index whitelist
const allowedIndices = ['my-index', 'my-index-staging'];
app.service('elasticsearch').hooks({
before: {
all: [
context => {
const requestedIndex = context.params.query?.$index;
if (requestedIndex && !allowedIndices.includes(requestedIndex)) {
throw new errors.Forbidden(`Access to index '${requestedIndex}' is not allowed`);
}
}
]
}
});Status: Known Issue
Severity: MEDIUM
Component: Object operations in multiple files
Description:
User-controlled object properties could potentially be used for prototype pollution attacks through document data or query parameters.
Mitigation:
// Sanitize input data
function sanitizeObject(obj) {
if (!obj || typeof obj !== 'object') return obj;
const dangerous = ['__proto__', 'constructor', 'prototype'];
const sanitized = {};
for (const key of Object.keys(obj)) {
if (dangerous.includes(key)) {
continue; // Skip dangerous keys
}
const value = obj[key];
sanitized[key] = typeof value === 'object' && value !== null
? sanitizeObject(value)
: value;
}
return sanitized;
}
app.service('elasticsearch').hooks({
before: {
create: [
context => {
context.data = sanitizeObject(context.data);
}
],
update: [
context => {
context.data = sanitizeObject(context.data);
}
],
patch: [
context => {
context.data = sanitizeObject(context.data);
}
]
}
});Status: Known Issue
Severity: MEDIUM (Development only)
Component: Development dependencies
Description:
npm audit identified 9 vulnerabilities in development dependencies. These do NOT affect production runtime but should be addressed for secure development environments.
Mitigation:
# Update dependencies
npm audit fix
# For unfixable issues, consider removing dtslint if not actively used
npm uninstall dtslint
# Add audit to CI/CD
npm audit --production # Only check production dependenciesApplications should implement rate limiting at the Feathers hooks level to prevent abuse.
Document size validation should be added for create/update operations.
The WeakMap cache could grow indefinitely in long-running processes. Consider implementing an LRU cache with TTL.
- Disable or restrict
raw()method access - Implement bulk operation limits (max 1,000-10,000 documents)
- Add query complexity validation
- Sanitize error messages in production
- Validate or disable
$indexfilter - Implement input sanitization for all create/update operations
- Run
npm audit fixfor development environment
- Enable authentication on all service methods
- Implement authorization hooks (e.g., feathers-casl)
- Add rate limiting
- Configure Elasticsearch client with SSL/TLS
- Set request timeouts (30 seconds recommended)
- Enable audit logging for sensitive operations
- Implement document size validation
- Add field whitelisting for
$sqsqueries - Set up automated security scanning in CI/CD
# Required environment variables
NODE_ENV=production
ELASTICSEARCH_URL=https://your-cluster:9200
ES_USERNAME=app_user
ES_PASSWORD=strong_password
# Security settings
MAX_BULK_OPERATIONS=1000
MAX_QUERY_DEPTH=50
MAX_DOCUMENT_SIZE=10485760 # 10MB
ENABLE_RAW_METHOD=falseConfigure your Elasticsearch client with security best practices:
import { Client } from '@elastic/elasticsearch';
const client = new Client({
node: process.env.ELASTICSEARCH_URL,
// Authentication
auth: {
username: process.env.ES_USERNAME,
password: process.env.ES_PASSWORD
},
// SSL/TLS
ssl: {
rejectUnauthorized: true, // Verify certificates
ca: fs.readFileSync('./ca.crt'), // CA certificate
},
// Performance and DoS protection
maxRetries: 3,
requestTimeout: 30000, // 30 second timeout
sniffOnConnectionFault: false, // Prevent node enumeration
maxSockets: 10, // Limit concurrent connections
maxFreeSockets: 5
});| Category | Count | Status |
|---|---|---|
| Critical Issues | 0 | β None found |
| High Severity | 3 | |
| Medium Severity | 4 | |
| Low Severity | 3 | βΉοΈ Optional improvements |
| Code Coverage | 94.21% | β Excellent |
| TypeScript Strict Mode | Enabled | β Excellent |
If you discover a security vulnerability in this package, please report it by:
- DO NOT open a public GitHub issue
- Email the maintainers directly at: [email protected]
- Include:
- Description of the vulnerability
- Steps to reproduce
- Potential impact
- Suggested fix (if any)
We will respond within 48 hours and work with you to address the issue.
- Elasticsearch Security Best Practices
- Feathers Authentication Documentation
- OWASP NoSQL Injection Guide
- Node.js Security Best Practices
- Initial security review completed
- Documented 3 high-severity issues
- Documented 4 medium-severity issues
- Added production deployment checklist
- Created mitigation examples
Security is a shared responsibility. This document provides guidance, but each application must implement appropriate security controls based on its specific requirements and threat model.