Hasura GraphQL: Introspection, Auth Bypass, and Admin Secret Cracking
Hasura's permissive defaults, introspection-by-default, and shared-secret admin model make it a recurring finding on B2B SaaS penetration tests. A deep dive into GraphQL security audit patterns, row-level permission failures, and the hardening checklist for production Hasura deployments.
Why Hasura keeps showing up on pentest scope
Hasura is a GraphQL compiler that sits in front of a Postgres database and auto-generates a full GraphQL API from your schema. Auth rules, joins, aggregations, mutations, subscriptions — all derived from the database. A single compose file gives you a production-grade GraphQL backend in about 90 seconds.
That speed is the reason it's everywhere. That speed is also why Hasura is one of the most common vulnerability assessment findings we see on client penetration testing engagements. The defaults are permissive. The hardening docs are buried. And the threat model is not what most teams assume it is.
This is a deep dive on the Hasura attack surface. Every finding here is something we've exploited in a real engagement or validated against our internal hasura-vuln lab.
The admin secret is not optional, but most teams treat it like it is
Hasura ships with a single "admin" role that bypasses all row-level permissions. The admin role is protected by a shared secret passed via the X-Hasura-Admin-Secret header. No rotation. No scoping. No expiry. You either have the secret or you don't.
The documentation default value in examples is myadminsecretkey. Search that string on Shodan. You will find hundreds of production Hasura instances using the literal documentation example.
Common patterns we exploit during a Hasura security audit:
- Empty admin secret.
HASURA_GRAPHQL_ADMIN_SECRETnot set. The console and API are unauthenticated. Any request becomes an admin request. - Default or leaked secret.
myadminsecretkey,secret,admin,hasura, or the old dev secret that got committed to a public repo and never rotated. - Exposed in client bundle. The admin secret accidentally loaded into the Next.js public env (
NEXT_PUBLIC_HASURA_ADMIN_SECRET), bundled, and shipped to every visitor. - JWT auth bypassed by console. If the console is reachable, it authenticates with the admin secret. Teams enable JWT for their app, leave the console public, and forget the admin-secret route exists.
Attack: curl -H "X-Hasura-Admin-Secret: myadminsecretkey" https://target/v1/graphql -d '{"query":"{ users { email password_hash } }"}' returns every row of every table.
Introspection is usually on, and that is usually your whole schema
GraphQL introspection is the feature that lets clients ask the API "what types and fields do you expose." It's essential during development. It should be off in production.
Hasura leaves it on by default. The config flag is HASURA_GRAPHQL_ENABLE_INTROSPECTION, and if you don't set it to false, introspection is enabled for every role including anonymous.
With introspection enabled, an unauthenticated attacker can dump the full schema — every type, every field, every argument, every mutation, every enum. Tools like GraphQL Voyager render it into a visual map. For a pentest, this collapses hours of endpoint enumeration into a 30-second download.
Field-level auth bypass via aliasing. Here's a pattern that keeps catching teams. Hasura lets you restrict specific fields per role. But GraphQL aliasing lets the client rename fields in the query. If the permission engine checks the original field name but the resolver handles the aliased name, you can read a restricted field by asking for it under a different alias.
query {
users {
stuff: password_hash # alias "stuff" reads password_hash
}
}
We've confirmed variants of this bug across multiple versions. The fix requires disabling introspection for the anonymous role specifically and tightening field-level permissions — not relying on alias-blind filters.
Row-level permissions: the tar pit
Hasura's row-level permissions are the primary access control layer for non-admin roles. You define per-role rules like "users can only SELECT their own rows" using a predicate expression. The predicates are stored as JSON, evaluated at query time, and compiled into SQL WHERE clauses.
Every real-world Hasura deployment we've audited has at least one of these row-level permission failures:
- Anonymous role with SELECT on sensitive tables. Teams enable
anonymousrole for their public landing page queries and forget the role has full SELECT onusers,invoices,api_keys, orsessions. - Permission predicate uses client-controlled variables without validation. A predicate like
{"user_id": {"_eq": "X-Hasura-User-Id"}}is safe. A predicate like{"org_id": {"_eq": "X-Hasura-Org-Id"}}where Org-Id comes from an attacker-modifiable JWT is not. - Missing check for linked tables. You add permissions on
ordersbut forget that orders has a foreign key join tocustomer_payment_methods. Hasura will happily traverse the join and expose payment methods. - Aggregate permissions misconfigured. Aggregates (COUNT, SUM, AVG) can leak information even when row-level SELECT is blocked. A user who can run
SELECT COUNT(*) WHERE email = 'target@example.com'can enumerate accounts one email at a time.
Insert permissions and mass assignment
Mutations with permissive insert permissions let clients write arbitrary fields. Common misconfigurations:
- The insert permission allows the role to write
role,is_admin,is_verified, orbalancefields. - The permission does not specify a
columnswhitelist, so every column is writable includingcreated_at,updated_at, and internal flags. - Foreign key columns are writable, allowing attackers to insert rows that reference other users' data.
mutation {
insert_users_one(object: {
email: "attacker@example.com"
password_hash: "$argon2id$..."
is_admin: true
verified: true
}) {
id
}
}
If is_admin is in the column list and the insert permission does not restrict it, this mutation creates an admin account.
Subscription amplification
Hasura's real-time subscriptions are WebSocket-based. Each subscription runs a live query against Postgres and pushes updates to the client. Subscriptions are per-row, so a subscription over 100K rows creates 100K live queries.
Attack pattern: open 50 subscriptions from the same client, each querying a large table with a permissive permission. Each one spawns Postgres processes, holds memory, and consumes connection pool slots. Hasura's default connection pool is 50. You can DoS a Hasura instance with 50-200 carefully crafted subscriptions from a single IP.
The fix is HASURA_GRAPHQL_MAX_OPEN_SUBSCRIPTIONS and per-role subscription limits. Most deployments ship without either.
Remote schemas: the forgotten attack surface
Hasura can stitch multiple GraphQL APIs together via remote schemas. This lets you expose a single GraphQL endpoint that proxies to microservices.
Remote schemas inherit Hasura's role system. But the remote schema itself may have a weaker auth model. If Hasura passes headers through to the remote without validating them, you can forge headers server-to-server.
We've seen real deployments where:
- A remote schema trusted an
X-Internal-Adminheader set by Hasura. - Hasura's forward_client_headers was enabled, passing client headers through to the remote.
- A client could send
X-Internal-Admin: truein a request to Hasura, and Hasura would forward it to the remote, which would grant admin privileges.
Always whitelist which headers forward to remote schemas. Never trust the remote to re-authenticate.
Actions: the SSRF vector
Hasura actions let you wire custom HTTP handlers into the GraphQL schema. You define a GraphQL mutation, map it to a webhook URL, and Hasura calls the webhook when the mutation fires.
The webhook URL is read from the action config at call time. If the URL is user-configurable (which it often is during development and sometimes in production), an attacker can point actions at internal services or cloud metadata endpoints.
POST /v1/metadata
{
"type": "create_action",
"args": {
"name": "ssrf_test",
"definition": {
"handler": "http://169.254.169.254/latest/meta-data/iam/security-credentials/",
"type": "mutation"
}
}
}
If the metadata endpoint is reachable (via admin secret or a misconfigured role with create_action permission), this creates a server-side request forgery (SSRF) vector that can reach AWS IMDSv1, GCP metadata, or internal microservices.
The hardening checklist
If your team runs Hasura, this is the bare minimum for a production Hasura security audit to pass:
- Set a strong admin secret. 32+ random characters. Rotate on every staff change. Never commit to version control.
- Disable console in production.
HASURA_GRAPHQL_ENABLE_CONSOLE=false. - Disable introspection for anonymous. Use
HASURA_GRAPHQL_ENABLE_INTROSPECTION=falseglobally or role-level introspection controls. - Restrict CORS.
HASURA_GRAPHQL_CORS_DOMAINset to your production origins only. No wildcards. - Enable query depth and complexity limits.
HASURA_GRAPHQL_MAX_NESTED_QUERY_DEPTHandHASURA_GRAPHQL_MAX_QUERY_COMPLEXITY. - Subscription limits.
HASURA_GRAPHQL_MAX_OPEN_SUBSCRIPTIONSand per-role limits. - Row-level permissions on every table. Default-deny, then grant. Never rely on "this table is internal."
- Column whitelists on every insert and update permission. No wildcards.
- JWT auth with short expiry. RS256 preferred over HS256. Audience claim validation.
- Separate admin endpoint. Put
/v1/metadatabehind a VPN or IP allowlist. - Remote schema header whitelisting. Never forward client headers blindly.
- Monitor for schema drift. Any unexpected metadata change = incident response trigger.
Why this matters for Valtik clients
Hasura powers a lot of B2B SaaS backends that have never been through an application security or penetration testing engagement. The vulnerabilities above show up in nearly every unaudited Hasura deployment we've tested. The pattern holds across Shopify app backends, analytics dashboards, internal admin tools, and API gateways.
If your stack includes Hasura and you haven't tested it against the full check list above, book a Valtik security audit. This is what we do.
Sources
- [Hasura Security Best Practices — Official Docs](https://hasura.io/docs/latest/security/security-best-practices/)
- [Hasura Admin Secret Configuration](https://hasura.io/docs/latest/deployment/graphql-engine-flags/config-examples/)
- [GraphQL Introspection Attacks — OWASP](https://cheatsheetseries.owasp.org/cheatsheets/GraphQL_Cheat_Sheet.html)
- [Hasura Auth & Permissions — Official Docs](https://hasura.io/docs/latest/auth/overview/)
- [Hasura Remote Schemas](https://hasura.io/docs/latest/remote-schemas/overview/)
- [Hasura Actions Security](https://hasura.io/docs/latest/actions/overview/)
- [Shodan search for default admin secrets](https://www.shodan.io/)
Want us to check your Hasura setup?
Our scanner detects this exact misconfiguration. plus dozens more across 38 platforms. Free website check available, no commitment required.
