Skip to content

Security: SSRF in document download endpoint — validate URL against allowlist #723

Description

@nadavosa

Problem

GET /volunteer/:id/doc/download?url=<arbitrary> (doc.routes.ts:47) fetches any URL the caller supplies and pipes the response back to the client, with no domain validation:

const { url } = request.query as { url: string };
const [urlObj, error] = await tryCatch(fetch(url));

Even though the route is COORDINATOR-only, a compromised coordinator account can use this as an SSRF proxy to:

  • Call http://169.254.169.254/latest/meta-data/iam/security-credentials/ (AWS IMDSv1) and steal the EC2 instance's IAM credentials
  • Probe internal services not reachable from the public internet
  • Read internal S3 presigned URLs or admin endpoints

Fix

Validate the supplied URL against a domain allowlist before fetching. Only S3 hostnames (and/or the configured CDN) should be accepted:

const ALLOWED_HOSTS = new Set([
  `${process.env.AWS_S3_BUCKET_NAME}.s3.eu-central-1.amazonaws.com`,
  `${process.env.AWS_S3_BUCKET_NAME}.s3.amazonaws.com`,
  // add CDN hostname if needed
]);

const parsed = new URL(url);
if (!ALLOWED_HOSTS.has(parsed.hostname)) {
  return reply.status(400).send({ message: "URL not allowed." });
}

Alternatively, once real presigned URL generation is in place, the backend can fetch directly from S3 using the stored s3Key instead of accepting a URL at all — eliminating the user-supplied URL entirely.

Context

Part of pre-NGO-onboarding security audit.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions