惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

P
Proofpoint News Feed
V
V2EX
博客园_首页
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Recent Announcements
Recent Announcements
博客园 - 司徒正美
Microsoft Security Blog
Microsoft Security Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Latest news
Latest news
Vercel News
Vercel News
The Register - Security
The Register - Security
T
The Exploit Database - CXSecurity.com
S
Schneier on Security
N
Netflix TechBlog - Medium
WordPress大学
WordPress大学
小众软件
小众软件
L
Lohrmann on Cybersecurity
GbyAI
GbyAI
P
Privacy & Cybersecurity Law Blog
T
Tor Project blog
AWS News Blog
AWS News Blog
美团技术团队
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
K
Kaspersky official blog
B
Blog RSS Feed
G
Google Developers Blog
量子位
大猫的无限游戏
大猫的无限游戏
Google DeepMind News
Google DeepMind News
Scott Helme
Scott Helme
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
I
Intezer
雷峰网
雷峰网
Martin Fowler
Martin Fowler
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Blog — PlanetScale
Blog — PlanetScale
IT之家
IT之家
F
Full Disclosure
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - 【当耐特】
The Hacker News
The Hacker News
U
Unit 42
S
SegmentFault 最新的问题
I
InfoQ
aimingoo的专栏
aimingoo的专栏
Y
Y Combinator Blog
宝玉的分享
宝玉的分享
罗磊的独立博客
Spread Privacy
Spread Privacy
C
CERT Recently Published Vulnerability Notes

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
GraphQL Authorization Bypass: A Real CVE Code Review
Stefan · 2026-05-17 · via DEV Community

Real-World GraphQL Authorization Bypass CVE Example Code Review

A tenant isolation bug in a GraphQL API differs from a REST IDOR in one uncomfortable way: the bypass often doesn't require a forged token, a path traversal, or a malformed request. The attacker sends a perfectly valid query, the server processes it correctly, and the authorization logic never fires because it was wired to the wrong layer. CVE-2023-26489 (wasmCloud host bypass) and a cluster of similar bugs in Apollo-based APIs share the same skeleton: query-root guards that protect the entry point while nested resolvers and aliases silently skip the check entirely.

How the GraphQL Authorization Bypass Works

GraphQL's resolver tree is the thing that makes this class of bug distinct. In a REST API, authorization lives in middleware that runs before the route handler — one route, one check. In GraphQL, a single HTTP POST to /graphql can resolve dozens of fields, each through its own resolver function. If you only check authorization at the root resolver (the entry point the client names in the query), every nested resolver below it inherits no protection by default.

The alias primitive makes this worse. A client can rename any field in their query with fieldName: actualField, which means rate limiting and allow-listing on field names breaks down immediately. Combined with fragments and batched operations, a single request can probe multiple objects across trust boundaries.

Here is the vulnerable Apollo Server pattern. Note there is no error handling on the attacker path — that's intentional, because the vulnerable code genuinely has none:

// resolvers.js — vulnerable pattern
const resolvers = {
  Query: {
    // Auth check here: only logged-in users reach this resolver
    me: (_, __, { user }) => {
      if (!user) throw new AuthenticationError("Not logged in");
      return user;
    },

    // No auth check at all — any caller can pass an arbitrary id
    user: (_, { id }) => {
      return db.users.findById(id);
    },
  },

  User: {
    // No ownership check — any User object returned above exposes this
    privateProfile: (parent) => {
      return db.profiles.findPrivateByUserId(parent.id);
    },

    email: (parent) => parent.email,
  },
};

Enter fullscreen mode Exit fullscreen mode

An attacker authenticated as user A sends this query to read user B's private data:

query StealProfile {
  victimData: user(id: "B") {
    email
    privateProfile {
      ssn
      dateOfBirth
      stripeCustomerId
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

The me guard never runs. user(id) returns whatever db.users.findById finds. The privateProfile resolver happily fetches the associated record because it only receives the parent User object — it has no way to know whether the caller owns that user unless you explicitly pass the request context and check it.

The alias (victimData: user(...)) is a red herring here — the bypass works without it. The alias just helps evade naive field-name logging and rate limits that watch for repeated user calls.

This is the pattern the GraphQL security code review lab on Code Review Lab uses to train reviewers to trace authorization through the full resolver tree, not just the query root.

The Fix: Field-Level Authorization in Resolvers

The minimal fix is to push the authorization check into every sensitive resolver so it executes regardless of how the field was reached — direct query, nested traversal, fragment, or alias.

Two patterns work well in production. The first is a context-aware check inside the resolver itself. The second is a schema directive that applies the same check declaratively and survives schema stitching.

// resolvers.js — patched
const { ForbiddenError } = require("apollo-server-errors");

const resolvers = {
  Query: {
    me: (_, __, { user }) => {
      if (!user) throw new AuthenticationError("Not logged in");
      return user;
    },

    user: (_, { id }, { user }) => {
      // Authenticated callers only — no anonymous traversal
      if (!user) throw new AuthenticationError("Not logged in");
      return db.users.findById(id);
    },
  },

  User: {
    privateProfile: (parent, _, { user }) => {
      // Ownership check lives here, not at the query root,
      // so it fires no matter which path reached this resolver
      if (!user || user.id !== parent.id) {
        throw new ForbiddenError("Access denied");
      }
      return db.profiles.findPrivateByUserId(parent.id);
    },

    email: (parent, _, { user }) => {
      // Even email is scoped — admins see all, owners see own
      if (!user) throw new AuthenticationError("Not logged in");
      if (user.role !== "ADMIN" && user.id !== parent.id) {
        throw new ForbiddenError("Access denied");
      }
      return parent.email;
    },
  },
};

Enter fullscreen mode Exit fullscreen mode

For teams that want the check to be impossible to accidentally omit during schema growth, graphql-shield applies rules as a middleware layer that wraps every resolver:

// permissions.js — graphql-shield rule tree
const { shield, rule, and } = require("graphql-shield");

const isAuthenticated = rule({ cache: "contextual" })(
  async (parent, args, ctx) => ctx.user !== null
);

const isOwner = rule({ cache: "strict" })(
  // parent here is the User object whose field is being resolved
  async (parent, args, ctx) => ctx.user?.id === parent.id
);

module.exports = shield({
  Query: {
    user: isAuthenticated,
  },
  User: {
    privateProfile: and(isAuthenticated, isOwner),
    email: and(isAuthenticated, isOwner),
  },
});

Enter fullscreen mode Exit fullscreen mode

The cache: "strict" on isOwner matters: it tells graphql-shield to re-evaluate the rule for every unique (parent, args, context) combination rather than short-circuiting on a previous result from the same request. Without it, a batched query that fetches your own profile first can warm the cache and let subsequent fields for other users pass through.

Reproducing the CVE Locally

The following setup pins a deliberately vulnerable Apollo Server configuration so you can confirm the attack, apply the patch, and re-run to verify the fix. Run this in a throwaway environment.

# docker-compose.yml
version: "3.9"
services:
  api:
    build: .
    ports:
      - "4000:4000"
    environment:
      NODE_ENV: development
      DB_SEED: "true"
    volumes:
      - ./src:/app/src  # mount local resolvers so hot-reload reflects your patch

Enter fullscreen mode Exit fullscreen mode

# seed creates two users: alice (id=1) and bob (id=2)
docker compose up --build

# Attack: authenticated as alice, read bob's privateProfile
curl -s -X POST http://localhost:4000/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $(cat tokens/alice.jwt)" \  # alice's valid JWT
  -d '{
    "query": "query { victimData: user(id: \"2\") { email privateProfile { ssn stripeCustomerId } } }"
  }' | jq .

Enter fullscreen mode Exit fullscreen mode

Pre-patch, this returns Bob's SSN and Stripe ID. Post-patch, the privateProfile resolver throws ForbiddenError before touching the database. The user query still resolves (Alice is authenticated), but the sensitive nested fields are blocked at their own resolvers.

One thing to watch: if your test token is an admin token, the patched code above will still return the data because the admin branch is intentional. Use a non-privileged user token when verifying the fix, or your test will produce a false negative.

Code Review Checklist for GraphQL Authz

When reviewing a GraphQL API PR, the question isn't "is there authentication?" — it's "does every resolver that touches sensitive data verify the caller's right to that specific parent object?"

- // Middleware approach (REST-era thinking, doesn't protect nested resolvers)
- app.use("/graphql", authenticate, graphqlHTTP({ schema }));

+ // Authorization inside each sensitive resolver
+ privateProfile: (parent, _, { user }) => {
+   if (!user || user.id !== parent.id) throw new ForbiddenError("Access denied");
+   return db.profiles.findPrivateByUserId(parent.id);
+ }

Enter fullscreen mode Exit fullscreen mode

Specific flags to raise during review:

Trust boundaries per resolver. Every resolver that reads or mutates data scoped to a specific user or tenant needs its own ownership or role check. A check only at the operation root is not sufficient.

Alias abuse surface. If the schema exposes user(id: ID!), any authenticated caller can query any user. Decide whether that's intentional. If not, remove the field or gate it behind an admin role — don't rely on clients not knowing the field exists.

Introspection in production. Introspection enabled on a production API hands the attacker the full field map. They don't need to guess privateProfile exists; the schema tells them. Apollo Server 3+ disables introspection in production by default; earlier versions don't.

Mutation scoping. Authorization bugs in queries leak data. Authorization bugs in mutations write or delete it. The same field-level pattern applies: a updateUser(id, data) mutation must verify the caller owns id, not just that they're authenticated.

Batched operations. Apollo's batching allows an array of operations in one POST. A resolver-level check handles this correctly; a per-request middleware check may only run once for the batch.

JWT claims as implicit trust. A JWT payload saying { "role": "ADMIN" } is only as trustworthy as the signature verification. If the server doesn't verify the signature (or uses alg: none), the entire permission model collapses. Verify claims server-side on every request; don't cache the decoded payload across requests in a way that could be shared across users.

The application security engineer path on Code Review Lab covers the full trust-boundary analysis methodology behind this kind of structured review.

Detecting the Pattern in CI

Static analysis can catch the most common form of this bug: a resolver function that never reads from context.user. It won't catch all cases — a resolver that reads context.user but doesn't compare it against parent.id still has the ownership bug — but it eliminates the obvious omissions before they merge.

# .github/workflows/graphql-security.yml
name: GraphQL Security Lint

on: [pull_request]

jobs:
  graphql-lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install dependencies
        run: npm ci

      - name: Run graphql-eslint with auth rules
        run: npx graphql-eslint --config .graphqlrc.yml src/**/*.graphql
        # Fails the build if any field resolver matches the no-auth pattern

      - name: Check resolver auth coverage
        run: node scripts/check-resolver-auth.js
        # Custom script: parses resolver map, flags any resolver
        # that returns sensitive types without referencing ctx.user

Enter fullscreen mode Exit fullscreen mode

// scripts/check-resolver-auth.js
// Parses resolver source with acorn, flags functions that touch
// db.profiles, db.payments, or db.pii without a ctx.user guard

const fs = require("fs");
const acorn = require("acorn");

const SENSITIVE_CALLS = ["findPrivateByUserId", "findPaymentByUserId", "findPiiByUserId"];
const resolverSource = fs.readFileSync("src/resolvers.js", "utf8");
const ast = acorn.parse(resolverSource, { ecmaVersion: 2022 });

// Walk AST looking for CallExpression nodes whose callee
// matches SENSITIVE_CALLS without a prior MemberExpression on ctx.user
// ... domain-specific AST traversal ...

process.exit(violationsFound ? 1 : 0);

Enter fullscreen mode Exit fullscreen mode

Wiring this into pull request checks means a new resolver that skips the auth guard fails the build before a reviewer even sees the diff. See how to secure your CI/CD pipeline for the broader pattern of making security checks load-bearing in the build process rather than advisory.

Hardening Beyond the Patch

Field-level auth fixes the specific bypass. These controls reduce the blast radius when the next one surfaces.

Persisted queries restrict the server to a pre-approved set of operations. An attacker can't construct an arbitrary aliased query if the server only executes queries you shipped. This doesn't replace authorization — a persisted query can still have an authz bug — but it eliminates the exploration phase.

Depth and complexity limits prevent deeply nested queries from being used to amplify data extraction or trigger DoS through resolver fan-out. Apollo Server provides both natively.

// apollo-server.js — defense-in-depth config
const { ApolloServer } = require("@apollo/server");
const depthLimit = require("graphql-depth-limit");
const { createComplexityLimitRule } = require("graphql-validation-complexity");

const server = new ApolloServer({
  schema,
  validationRules: [
    depthLimit(7),                              // reject queries nested deeper than 7 levels
    createComplexityLimitRule(1000, {           // reject queries with complexity score > 1000
      onCost: (cost) => console.log("Query cost:", cost),
    }),
  ],
  introspection: process.env.NODE_ENV !== "production", // disable introspection in prod
  persistedQueries: {
    cache: persistedQueriesCache, // APQ: only execute known query hashes
  },
});

Enter fullscreen mode Exit fullscreen mode

Multi-tenant schema design deserves explicit threat modeling. If your schema includes organization(id: ID!) and user(id: ID!) as top-level queries, consider whether tenant-scoping should be enforced at the data layer (row-level security in Postgres, for example) rather than relying entirely on resolver logic. Resolver logic can be forgotten; a database constraint cannot.

If you're building or evaluating APIs beyond GraphQL, the same field-level trust boundary analysis applies to gRPC API security patterns — service-level auth in gRPC has the same failure mode as query-root-only auth in GraphQL.

Key Takeaways

The bypass primitive is consistent across every instance of this bug class: authorization is enforced at the operation entry point but not at every resolver that handles sensitive data. The field-level check is the minimal viable fix. The directive or middleware approach (graphql-shield, schema directives) scales better than per-resolver if/throw blocks as the schema grows, because it makes authorization visible in the schema definition rather than scattered across resolver implementations.

The hardest part of reviewing GraphQL for this pattern is tracing every path that can reach a sensitive type — aliases, fragments, and inline fragments all create paths that bypass a naive "is this field name protected?" check. Ownership checks tied to parent.id vs context.user.id inside the resolver itself are the only reliable guard.

If you want structured practice finding this in realistic code, practice spotting this in interview-style reviews on Code Review Lab or work through the full GraphQL security lab against a live vulnerable target — reading the pattern once and hunting it under time pressure are different skills.

Further Reading