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: security@feathersjs.com
- 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.