Ssrf Server Side in Feathersjs with Hmac Signatures
Ssrf Server Side in Feathersjs with Hmac Signatures — how this specific combination creates or exposes the vulnerability
Server-Side Request Forgery (SSRF) in FeathersJS when HMAC signatures are used for external HTTP requests arises when an attacker can influence the URL or parameters of a service call while the server computes and applies HMAC signatures. FeathersJS services often make outbound calls to third‑party APIs, and if the endpoint, host, or query values are derived from user input without strict validation, an SSRF vector can be introduced. The HMAC mechanism itself does not prevent SSRF; it only authenticates the request to the downstream API. An attacker can coerce the server into signing and forwarding requests to internal endpoints (e.g., http://127.0.0.1:metadata, http://169.254.169.254/latest/meta-data) or to arbitrary internal services that the server can reach. The signature is computed over the request parameters or headers agreed with the external API, but if the URL is user-supplied, the signature may still be valid for the malicious target, allowing the server to relay the request. Common triggers include dynamic base URLs, concatenated paths, or query parameters that are not strictly constrained. Because SSRF abuses the server’s network reachability, the impact can include metadata service access, internal service enumeration, or pivoting to other internal resources. FeathersJS hooks and services must validate and sanitize all inputs that affect the request target, regardless of whether HMAC is used for authorization.
Hmac Signatures-Specific Remediation in Feathersjs — concrete code fixes
To mitigate SSRF while using HMAC signatures in FeathersJS, enforce strict allowlists on endpoints, avoid concatenating untrusted input into URLs, and treat the signature scope as covering the canonical request rather than as a bypass for input validation. Below are concrete code examples that demonstrate a secure pattern.
Secure HMAC request with a fixed, allowlisted endpoint
Define a whitelist of allowed hosts and paths. Compute the HMAC only over validated, normalized components.
const crypto = require('crypto');
// Allowed targets: only specific known services
const ALLOWED_TARGETS = new Set([
'https://api.example.com/v1/resource',
'https://api.example.com/v1/other'
]);
function signPayload(payload, secret) {
return crypto.createHmac('sha256', secret).update(payload).digest('hex');
}
function makeSignedRequest({ targetUrl, userParams, secret }) {
if (!ALLOWED_TARGETS.has(targetUrl)) {
throw new Error('Request target not allowed');
}
const url = new URL(targetUrl);
// Normalize and include only safe query parameters
const params = new URLSearchParams();
if (userParams && userParams.id) {
params.set('id', String(userParams.id));
}
// Ensure no unexpected query keys
url.search = params.toString();
const payload = url.pathname + '?' + url.search;
const signature = signPayload(payload, secret);
return fetch(`${url}?sig=${signature}`, {
method: 'GET',
headers: {
'X-Signature-Algorithm': 'HS256'
}
});
}
// Usage within a Feathers service hook or custom service
app.service('external').find({
query: { id: '123' }
}).then((result) => {
// process result
});
// In a hook, validate and sign
app.hooks({
before: {
create: [context => {
const { targetUrl, userParams } = context.data;
context.data.signedRequest = makeSignedRequest({ targetUrl, userParams, secret: process.env.HMAC_SECRET });
return context;
}]
}
});
Dynamic host validation and canonicalization
When the host must be dynamic (e.g., multi-tenant), validate against a registry and canonicalize the URL before signing. Include the request method and selected headers in the signature scope to prevent method smuggling or header injection.
function canonicalizeRequest({ method, url, selectedHeaders }) {
const u = new URL(url);
// Only allow specific host patterns
if (!u.hostname.endsWith('.trusted-provider.com')) {
throw new Error('Host not trusted');
}
// Exclude sensitive headers from signature
const filteredHeaders = Object.entries(selectedHeaders || {})
.filter(([k]) => k.toLowerCase() !== 'authorization')
.sort(([a], [b]) => a.localeCompare(b));
const headerString = filteredHeaders.map(([k, v]) => `${k}:${v}`).join('|');
return `${method.toUpperCase()}|${u.pathname}|${u.search}|${headerString}`;
}
function buildSignedUrl({ method, url, headers, secret }) {
const canonical = canonicalizeRequest({ method, url, headers });
const signature = signPayload(canonical, secret);
return { url, headers: { ...headers, 'X-Signature': signature, 'X-Canonical': canonical } };
}
// Example usage
const prepared = buildSignedUrl({
method: 'GET',
url: 'https://api.trusted-provider.com/v1/data',
headers: { 'X-Client-ID': 'abc' },
secret: process.env.HMAC_SECRET
});
console.log(prepared);
Integrate with FeathersJS services and hooks
Use a custom service or hook to enforce validation before making the external call. Do not allow arbitrary URLs from clients.
const { Forbidden } = require('@feathersjs/errors');
app.service('external-proxy').hooks({
before: {
create: [async context => {
const { target, params } = context.data;
// Strict allowlist for target path segments
if (!/^[a-z0-9-]+$/.test(target)) {
throw new Forbidden('Invalid target');
}
const base = 'https://api.partner.com';
const path = `/v1/${encodeURIComponent(target)}`;
const url = new URL(path, base);
// Validate query schema
if (url.searchParams.has('redirect_uri')) {
throw new Forbidden('Unexpected parameter');
}
const payload = path + '?' + url.search;
const signature = crypto.createHmac('sha256', process.env.HMAC_SECRET).update(payload).digest('hex');
context.params.headers = { 'X-Signature': signature };
context.data.finalUrl = `${url}?sig=${signature}`;
return context;
}]
},
after: {
create: [async context => {
// Inspect response for PII, keys, code — example pattern check
const text = context.result.data || '';
if (/\b[A-Za-z0-9+/]{40}\b/.test(text)) {
throw new Error('Potential secret leakage in response');
}
return context;
}]
}
});
These patterns ensure that HMAC-signed requests in FeathersJS do not become an SSRF channel by tightly constraining targets, validating hosts, and including method and selected headers in the signature scope. The server still detects and reports issues; developers must apply these guards to prevent abuse.