HIGH ssrf server sidefeathersjshmac signatures

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.

Frequently Asked Questions

Can HMAC signatures prevent SSRF in FeathersJS?
No. HMAC signatures authenticate requests to external APIs but do not restrict which internal or external hosts the server can reach. You must validate and allowlist targets independently.
What input should be restricted to reduce SSRF risk in FeathersJS services?
Restrict URLs, hosts, and path components used in outbound requests; avoid concatenating user input into destinations; and maintain an allowlist of permitted endpoints.