GraphQL har blivit förstahandsvalet för många svenska och nordiska team som bygger nya API:er, men flexibiliteten kostar något. Under 2025 och 2026 har säkerhetsforskare hittat allvarliga brister i populära GraphQL-servrar, från Apollo Router till GitLabs egen implementation, med allt från överbelastningsattacker till trasig auktorisering. Den här guiden går igenom hur du bygger in skydd mot introspection-läckor, djupa frågor, batching-attacker och behörighetsfel i en Node.js-baserad GraphQL-API. Du får kod du kan använda direkt, i tolv konkreta steg, en färdig referensserver att kopiera och en lista över de CVE:er som förklarar varför varje steg finns med.

Guiden är skriven för backend- och fullstack-utvecklare som redan har ett fungerande GraphQL-schema, eller som planerar att bygga ett, och som vill lägga ett säkerhetslager ovanpå innan tjänsten går live. Räkna med att avsätta cirka en timme för att gå igenom samtliga tolv steg och testa dem mot din egen kod.

Varför GraphQL-säkerhet är avgörande just nu

GraphQL byter ut REST:s många fasta endpoints mot en enda flexibel ingång, och den flexibiliteten är precis vad som gör tekniken attraktiv för frontend-team. Samma flexibilitet ger dock angripare fler vägar in. En klient kan i teorin be om hela datamodellen i en enda förfrågan, kombinera dussintals fält och köra flera operationer parallellt i samma anrop.

Sårbarhetsstatistiken bekräftar mönstret. Apollo GraphQL-produkter fick fem publicerade sårbarheter under 2025 med ett genomsnittligt CVSS-värde på 8,2, upp från fyra sårbarheter 2024 med snittvärdet 7,03. I april 2025 avslöjade GitLab CVE-2025-3922, en brist i sitt GraphQL-API som lät angripare tömma serverresurser genom ohämmade förfrågningar. Bristen påverkade GitLab-versioner från 12.4 till 18.11.0 innan den patchades i 18.9.6, 18.10.4 och 18.11.1.

Bredare API-statistik pekar åt samma håll. Enligt en sammanställning av Salt Securitys API-rapport upplevde 99 procent av organisationerna någon typ av API-säkerhetsproblem under det senaste året, och 55 procent uppgav att de saktat ner lanseringen av nya applikationer på grund av just säkerhetsoro. Av de faktiska incidenterna handlade 37 procent om sårbarheter, 34 procent om läckor av känslig data och 29 procent om autentiseringsproblem. Bland utnyttjade OWASP-kategorier stod felkonfiguration (API8) för 54 procent av attackerna och trasig objektnivåauktorisering (API1, BOLA) för 27 procent, två kategorier som drabbar GraphQL-API:er extra hårt eftersom ett enda felkonfigurerat schema kan exponera hela datamodellen på en gång.

För nordiska utvecklingsteam, som ofta bygger API:er kopplade mot BankID, betalningsflöden eller myndighetsdata, är den typen av exponering inte ett teoretiskt problem. Den här artikeln fokuserar på fem konkreta hot: introspection-läckor, djupa eller komplexa frågor, batching-attacker, trasig auktorisering och injektion, och visar hur du stänger varje lucka i en Node.js-stack.

Det finns också en regulatorisk dimension. Företag som omfattas av NIS2-lagstiftningen måste kunna visa att de har processer för att hantera sårbarheter i system som exponerar persondata eller kritisk infrastruktur, och ett GraphQL-API som saknar grundläggande skydd mot resursutmattning eller trasig auktorisering är precis den typen av brist en tillsynsmyndighet kan fästa sig vid vid en incidentgranskning. Att bygga in skydden från start är billigare än att förklara varför de saknades efteråt.

GraphQL kontra REST: nya attackytor att känna till

REST-API:er begränsar naturligt vad en klient kan fråga efter, eftersom varje endpoint returnerar en fördefinierad datastruktur. GraphQL vänder på det. Klienten bestämmer formen på svaret, vilket är kraftfullt för frontend-utveckling men flyttar ansvaret för att begränsa frågor till backend-teamet.

EgenskapRESTGraphQL
Antal endpointsMånga, fastaEn, dynamisk
FrågedjupBegränsat av designObegränsat om inget sätts uttryckligen
IntrospectionFinns inte som konceptAktiverat som standard i utveckling
Batching av flera operationerKräver flera separata anropInbyggt i en enda förfrågan
Rate limitingPer endpointMåste räkna kostnad per fält
FelmeddelandenVarierar per endpointKan läcka hela schemat om inget begränsas

Den viktigaste skillnaden är att en enda skadlig GraphQL-förfrågan kan motsvara hundratals REST-anrop i resursförbrukning, eftersom nästlade fält och alias låter en klient multiplicera antalet databasslagningar kraftigt inom en och samma request. Det är därför djupbegränsning och kostnadsanalys, som du bygger i steg 4 och 5, är grundläggande snarare än valfria tillägg för en produktions-API.

Så här ser en verklig attackfråga ut

Nedan är ett förenklat exempel på hur en angripare kan utnyttja nästlade relationer och alias för att multiplicera arbetsbördan på servern. Frågan ser oskyldig ut för en människa som läser den snabbt, men varje alias tvingar fram en ny genomkörning av samma dyra relation.

query Overbelastning {
  a: user(id: "1") { friends { friends { friends { name } } } }
  b: user(id: "1") { friends { friends { friends { name } } } }
  c: user(id: "1") { friends { friends { friends { name } } } }
  # ... upprepat med fler alias, i praktiken ofta hundratals
}

Utan skydd exekverar servern varje alias som en helt separat operation, vilket i det här fallet innebär tre nivåer av vännerrelationer multiplicerat med antalet alias. Det är exakt den typen av mönster som låg bakom resursförbrukningsproblemen i GitLabs och Apollo Routers CVE:er, och som steg 4 till 7 nedan bygger skydd mot.

Förutsättningar: verktyg, paket och versioner

Innan du börjar behöver du följande installerat. Versionerna nedan är de senaste stabila utgåvorna den 22 augusti 2026, verifierade direkt mot npm-registret och Node.js officiella utgivningsindex.

Verktyg/paketVersionSyfte
Node.js24.19.0 LTS (Krypton)Körmiljö
npm12.0.2Pakethantering
graphql17.0.2Kärnbibliotek för schema och exekvering
@apollo/server5.5.1GraphQL-server ovanpå Express
express5.2.1HTTP-ramverk
graphql-depth-limit1.1.0Begränsa frågedjup
graphql-query-complexity2.0.0Poängsätta och begränsa frågekostnad
express-rate-limit8.6.2Hastighetsbegränsning per klient

Du behöver också grundläggande kunskap i Node.js och Express, en editor och en terminal. Exemplen i den här guiden använder JavaScript med ES-moduler, men samma mönster fungerar identiskt i TypeScript. Du behöver ingen riktig databas för att följa med, eftersom exemplen använder en mockad datakälla i minnet så att du kan fokusera på säkerhetslagren i stället för på databasinstallation.

Kontrollera gärna dina installerade versioner innan du fortsätter, särskilt om du lägger paketen till ett befintligt projekt snarare än att starta från noll. Föråldrade versioner av graphql eller @apollo/server saknar ofta säkerhetsfixar från de CVE:er som beskrivs längre ner i guiden.

npm list graphql @apollo/server express graphql-depth-limit graphql-query-complexity --depth=0
npm outdated

Steg 1–2: Skapa ett säkert Apollo Server-projekt

Börja med att skapa projektmappen och installera paketen från förutsättningstabellen ovan.

mkdir graphql-sakerhet-demo && cd graphql-sakerhet-demo
npm init -y
npm install @apollo/server graphql express express-rate-limit graphql-depth-limit graphql-query-complexity cors

Skapa sedan ett schema i schema.graphql. Håll det minimalt till att börja med, du bygger ut det när du testar säkerhetslagren.

type User {
  id: ID!
  name: String!
  email: String!
  role: String!
}

type Query {
  users: [User!]!
  user(id: ID!): User
}

Skriv sedan grundservern i index.js. Den här varianten fungerar, men saknar helt de skydd du lägger till i resten av guiden, ungefär som en standardinstallation gör om ingen aktivt hårdar den.

import express from 'express';
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { readFileSync } from 'node:fs';

const typeDefs = readFileSync('./schema.graphql', 'utf-8');

const users = [
  { id: '1', name: 'Anna Karlsson', email: '[email protected]', role: 'admin' },
  { id: '2', name: 'Erik Svensson', email: '[email protected]', role: 'user' },
];

const resolvers = {
  Query: {
    users: () => users,
    user: (_parent, { id }) => users.find((u) => u.id === id),
  },
};

const server = new ApolloServer({ typeDefs, resolvers });
await server.start();

const app = express();
app.use('/graphql', express.json(), expressMiddleware(server));

app.listen(4000, () => {
  console.log('GraphQL-server igång på http://localhost:4000/graphql');
});

Den här basversionen har introspection påslagen som standard och inga gränser för frågedjup, kostnad eller antal anrop. Det är i praktiken samma utgångsläge som gjorde att GitLabs och Apollos egna sårbarheter kunde utnyttjas i produktion. Nästa steg täpper till luckorna en i taget.

Steg 3: Stäng av introspection och inbyggda GraphQL-verktyg i produktion

Introspection låter en klient fråga schemat om sig själv, vilket är ovärderligt under utveckling men en gratiskarta för en angripare i produktion. OWASP:s GraphQL Cheat Sheet rekommenderar uttryckligen att introspection stängs av eller begränsas utanför utvecklingsmiljön. Det finns också en konkret anledning att hålla interaktiva verktyg borta från produktion: i november 2025 publicerades CVE-2025-59845, en CSRF-sårbarhet i Apollo Studios inbäddade Explorer och Sandbox där valideringen av window.postMessage-ursprung kunde kringgås, vilket påverkade Sandbox till och med version 2.7.2 och Explorer till och med 3.7.3.

import { ApolloServerPluginLandingPageDisabled } from '@apollo/server/plugin/disabled';

const isProd = process.env.NODE_ENV === 'production';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: !isProd,
  plugins: isProd ? [ApolloServerPluginLandingPageDisabled()] : [],
});

Testa genom att skicka en introspection-fråga mot din produktionsmiljö. Rätt konfigurerad ska servern svara med ett fel snarare än schemat:

{
  "errors": [
    {
      "message": "GraphQL introspection is not allowed, but the query contained __schema or __type",
      "extensions": { "code": "GRAPHQL_VALIDATION_FAILED" }
    }
  ]
}

Ett vanligt misstag här är att bara sätta NODE_ENV lokalt men glömma det i CI/CD-pipelinen eller i containerns miljövariabler, vilket gör att produktionsservern av misstag startar med introspection påslagen. Verifiera alltid variabeln i den faktiska driftmiljön, inte bara i din lokala .env-fil.

Steg 4: Begränsa frågedjup med graphql-depth-limit

Djupattacker utnyttjar nästlade relationer, till exempel att be om vänners vänners vänner rekursivt, för att tvinga fram exponentiellt fler databasslagningar per förfrågan. I november 2025 visade CVE-2025-31496 i Apollo Compiler att även valideringssteget kan bli en attackyta: djupt nästlade och återanvända namngivna fragment kunde göra själva valideringen orimligt dyr att köra, alltså en överbelastningsrisk innan frågan ens exekverats.

import depthLimit from 'graphql-depth-limit';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [depthLimit(6)],
});

Gränsen 6 är en utgångspunkt, inte en universallösning. Logga det faktiska djupet i din riktiga produktionstrafik under en vecka innan du låser gränsen, så undviker du att blockera legitima frontend-frågor som råkar vara djupare än du trodde. En förfrågan som överskrider gränsen ger ett tydligt fel:

{
  "errors": [
    { "message": "Query is too deep. Maximum depth is 6, but got 9." }
  ]
}

Steg 5: Kostnadsanalys mot resursutmattning

Djupbegränsning stoppar inte en bred förfrågan som ligger platt men drar hundratals fält på samma nivå. Det är precis den typen av brist som ligger bakom flera av Apollo Routers sårbarheter under 2024 och 2025: CVE-2024-43783 (överbelastning via coprocessors), CVE-2025-32380 och CVE-2025-32034 (resursförbrukning via namngiven fragmentexpansion i frågeplaneraren) samt CVE-2025-32031, som i april 2025 beskrevs som en kringgång av resursoptimering i Apollo Gateway. Lösningen är att räkna en kostnad per fält, inte bara ett djup.

import { createComplexityRule, simpleEstimator, fieldExtensionsEstimator } from 'graphql-query-complexity';

const complexityRule = createComplexityRule({
  maximumComplexity: 1000,
  estimators: [
    fieldExtensionsEstimator(),
    simpleEstimator({ defaultComplexity: 1 }),
  ],
  onComplete: (complexity) => {
    console.log('Query complexity:', complexity);
  },
});

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [depthLimit(6), complexityRule],
});

Sätt högre kostnad på fält som utlöser dyra operationer, till exempel listor med paginering eller fritextsökning, jämfört med enkla skalära fält.

FälttypUppskattad kostnadExempel
Enkelt skalärt fält1id, name, email
Relation (lista)10 × antal hämtade postercomments(first: 50)
Fritextsökning eller filter20search(query: “…”)
Mutation5createPost

Steg 6–7: Stoppa batching-attacker och sätt rate limits per klient

GraphQL tillåter flera operationer i en enda HTTP-förfrågan, ofta kallat batching. Kombinerat med alias, där samma fält kan begäras flera gånger under olika namn i samma dokument, kan en klient skicka det som i praktiken är hundratals frågor förklädda till ett enda anrop, vilket kringgår en rate limiter som bara räknar HTTP-requests. Håll batching avstängt om du inte har ett uttalat behov av det, och sätt alltid ett tak på antalet operationer per batch om du aktiverar funktionen.

Complexity-regeln från steg 5 hjälper delvis mot alias-attacker också, eftersom varje alias räknas som en egen instans av fältet och därmed adderar till den totala kostnaden. Se ändå till att din estimator faktiskt itererar över alla alias i dokumentet snarare än att bara titta på det unika schemat, annars riskerar du att komplexitetsberäkningen underskattar en förfrågan som upprepar samma dyra fält under tjugo olika alias.

Lägg sedan på en rate limiter som skyddar hela GraphQL-endpointen. Notera att detta inte ersätter kostnadsanalysen i föregående steg, det är ett kompletterande lager mot rena volymattacker.

import rateLimit from 'express-rate-limit';

const graphqlLimiter = rateLimit({
  windowMs: 60 * 1000,
  max: 60,
  standardHeaders: true,
  legacyHeaders: false,
  keyGenerator: (req) => req.headers['x-api-key'] || req.ip,
  message: { errors: [{ message: 'För många förfrågningar, försök igen om en minut.' }] },
});

app.use('/graphql', graphqlLimiter, express.json(), expressMiddleware(server));

Ett svar från en klient som slår i taket ser ut så här, med HTTP-status 429:

HTTP/1.1 429 Too Many Requests
Retry-After: 42

{ "errors": [{ "message": "För många förfrågningar, försök igen om en minut." }] }

Steg 8: Auktorisering i varje resolver, inte bara i gatewayen

I november 2025 publicerades CVE-2025-64530, klassad CWE-288 med CVSS-poäng 7,5, i Apollos federationsbibliotek @apollo/composition. Bristen handlade om bristande efterlevnad av åtkomstkontroll på interface-typer och fält, ett tydligt exempel på att auktorisering som bara ligger på schemanivå eller i en gateway inte räcker. Salt Securitys säkerhetsforskare har tidigare dokumenterat ett liknande verkligt fall hos en B2B-fintechplattform, där bristande auktorisering i GraphQL-resolvrar exponerade känslig data trots att API:et såg välformat ut utåt.

Lösningen är att kontrollera behörighet i varje resolver som rör känslig data, före datahämtningen, inte efter.

import { GraphQLError } from 'graphql';

const resolvers = {
  Query: {
    user: (_parent, { id }, context) => {
      if (!context.user) {
        throw new GraphQLError('Inte autentiserad', {
          extensions: { code: 'UNAUTHENTICATED' },
        });
      }
      if (context.user.role !== 'admin' && context.user.id !== id) {
        throw new GraphQLError('Saknar behörighet för denna resurs', {
          extensions: { code: 'FORBIDDEN' },
        });
      }
      return users.find((u) => u.id === id);
    },
  },
};

Kontexten (context.user) sätts genom att verifiera en JWT i Apollo Servers context-funktion vid varje förfrågan, samma mönster som beskrivs i vår guide till API-säkerhet med JWT mot BOLA-attacker, fast tillämpat per fält snarare än per endpoint.

För scheman med många känsliga fält växer den här typen av manuell kontroll snabbt. Ett alternativ är ett eget schema-direktiv, till exempel @auth(requires: ADMIN), som du kopplar till ett schema-transform vid serverstart. Direktivet flyttar behörighetsreglerna från spridd kod i varje resolver till en enda, granskningsbar plats i schemat, vilket gör det lättare att upptäcka om ett nytt fält av misstag saknar skydd innan det når produktion.

Steg 9: Indatavalidering och injektionsskydd

GraphQLs typade schema skyddar dig inte automatiskt mot injektion. Argument skickas fortfarande vidare till din databas, och om du bygger frågesträngar genom att klistra ihop indata direkt öppnar du samma sårbarhet som i vilket REST-API som helst.

// Osäkert: bygger SQL-strängen direkt av användarindata
const rows = await db.query(`SELECT * FROM users WHERE email = '${args.email}'`);

// Säkert: parametriserad fråga, drivrutinen hanterar citattecken och specialtecken
const rows = await db.query('SELECT * FROM users WHERE email = $1', [args.email]);

Komplettera med striktare skalartyper för fält som e-post, personnummerliknande data eller fritextsökning, så att felaktig indata stoppas redan vid valideringen och aldrig når din resolverkod. Vår guide till Node.js-indatavalidering mot injektionsattacker går igenom mönstret mer i detalj för REST, och samma bibliotek går att återanvända i GraphQL-resolvrar.

Samma princip gäller om din datakälla är en NoSQL-databas som MongoDB. Ett argument som skickas rakt in i en $where-fråga eller ett operatoruttryck utan att typkontrolleras kan låta en angripare injicera egna operatorer i stället för ett värde. Håll dig till strukturerade frågeobjekt där varje fält har en explicit typ, och lita aldrig på att GraphQLs schemavalidering ensam fångar den typen av manipulerad indata, den kontrollerar bara formen på datan, inte dess innehåll eller avsikt.

Steg 10–12: Persisted queries, loggning och säkerhetstestning

Persisted queries som allowlist i produktion

Med persisted queries skickar klienten bara ett hash av frågan, inte hela frågetexten, och servern kör enbart frågor den känner igen sedan tidigare. I praktiken blir det en allowlist som stoppar ad hoc-konstruerade, komplexa frågor från att någonsin nå din exekveringsmotor, samtidigt som det minskar bandbredden.

import { InMemoryLRUCache } from '@apollo/utils.keyvaluecache';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  persistedQueries: {
    cache: new InMemoryLRUCache(),
  },
});

Loggning och säkerhetstestning innan lansering

Logga varje avvisad förfrågan, alltså allt som fastnar i djupgränsen, kostnadsgränsen, rate limitern eller auktoriseringskontrollen, tillsammans med ett request-id och avsändar-IP. Ett plötsligt kluster av avvisade frågor är ofta det första tecknet på att någon aktivt sonderar ditt schema.

Testa sedan schemat aktivt innan lansering. Skicka manuella introspection-frågor mot din stagingmiljö med curl, kör igenom OWASP:s GraphQL Cheat Sheet som checklista, och överväg ett dedikerat verktyg som skannar av vanliga GraphQL-specifika brister (introspection, batching, alias-överbelastning) automatiskt som en del av CI-pipelinen.

Tre praktiska sätt att testa innan release: kör Burp Suite-tillägget InQL för att automatiskt bygga ut ditt schema och generera testfrågor, använd Postmans inbyggda stöd för GraphQL-introspektion för att manuellt utforska vilka fält som exponeras, och skriv egna lasttester som skickar gradvis djupare och bredare frågor mot en staging-server för att se exakt var dina gränser i praktiken slår till. Spara resultaten som referens, så märker du snabbt om en framtida schemaändring råkar öppna en ny lucka.

Komplett exempel: hela säkerhetslagret i en fil

Nedan är alla lager från steg 3 till 9 sammanslagna i en fungerande index.js. Använd den som utgångspunkt eller referenspunkt när du kopplar in samma mönster i ditt eget schema.

import express from 'express';
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { ApolloServerPluginLandingPageDisabled } from '@apollo/server/plugin/disabled';
import { readFileSync } from 'node:fs';
import depthLimit from 'graphql-depth-limit';
import { createComplexityRule, simpleEstimator, fieldExtensionsEstimator } from 'graphql-query-complexity';
import rateLimit from 'express-rate-limit';
import { GraphQLError } from 'graphql';
import { verifyToken } from './auth.js';

const typeDefs = readFileSync('./schema.graphql', 'utf-8');
const isProd = process.env.NODE_ENV === 'production';

const users = [
  { id: '1', name: 'Anna Karlsson', email: '[email protected]', role: 'admin' },
  { id: '2', name: 'Erik Svensson', email: '[email protected]', role: 'user' },
];

const complexityRule = createComplexityRule({
  maximumComplexity: 1000,
  estimators: [fieldExtensionsEstimator(), simpleEstimator({ defaultComplexity: 1 })],
  onComplete: (complexity) => console.log('Query complexity:', complexity),
});

const resolvers = {
  Query: {
    users: (_parent, _args, context) => {
      if (!context.user) throw new GraphQLError('Inte autentiserad', { extensions: { code: 'UNAUTHENTICATED' } });
      return users;
    },
    user: (_parent, { id }, context) => {
      if (!context.user) throw new GraphQLError('Inte autentiserad', { extensions: { code: 'UNAUTHENTICATED' } });
      if (context.user.role !== 'admin' && context.user.id !== id) {
        throw new GraphQLError('Saknar behörighet', { extensions: { code: 'FORBIDDEN' } });
      }
      return users.find((u) => u.id === id) || null;
    },
  },
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: !isProd,
  plugins: isProd ? [ApolloServerPluginLandingPageDisabled()] : [],
  validationRules: [depthLimit(6), complexityRule],
  persistedQueries: isProd ? { cache: new Map() } : false,
});
await server.start();

const app = express();
const graphqlLimiter = rateLimit({
  windowMs: 60 * 1000,
  max: 60,
  keyGenerator: (req) => req.headers['x-api-key'] || req.ip,
  message: { errors: [{ message: 'För många förfrågningar, försök igen om en minut.' }] },
});

app.use(
  '/graphql',
  graphqlLimiter,
  express.json(),
  expressMiddleware(server, {
    context: async ({ req }) => ({ user: await verifyToken(req.headers.authorization) }),
  })
);

app.listen(4000, () => {
  console.log(`GraphQL-server igång i ${isProd ? 'produktionsläge' : 'utvecklingsläge'} på http://localhost:4000/graphql`);
});

Notera hur varje resolver som rör användardata kontrollerar context.user innan den ens rör datakällan, hur validationRules kombinerar djup- och kostnadsgränser i samma lista, och hur persistedQueries bara aktiveras när servern faktiskt kör i produktionsläge. Det är samma tolv steg som gåtts igenom ovan, bara sammanslagna till en fil du kan klona som startpunkt för din egen tjänst.

Verkliga CVE:er som visar riskerna med osäkrad GraphQL

Tabellen nedan samlar de sårbarheter som nämns i den här guiden, allt bekräftade och publicerade under 2024–2025.

CVEProduktTyp av bristPublicerad
CVE-2025-3922GitLab GraphQL APIOhämmad resursförbrukning (DoS)April 2025
CVE-2024-32971Apollo RouterFel i query plan-cache, dataexponering (kritisk)2024
CVE-2024-43783Apollo Router (coprocessors)Överbelastning (DoS)2024
CVE-2024-43414Apollo Query Planner/GatewayOändlig loop vid komplexa frågor (hög)2024
CVE-2025-32380Apollo RouterResursförbrukning via fragmentbehandling2025
CVE-2025-32034Apollo Router frågeplanerareResursförbrukning via fragmentexpansion2025
CVE-2025-32031Apollo GatewayKringgång av resursoptimeringApril 2025
CVE-2025-31496Apollo CompilerDoS vid validering av nästlade fragment2025
CVE-2025-64530Apollo Federation (composition)Trasig åtkomstkontroll, CVSS 7,5November 2025
CVE-2025-59845Apollo Sandbox/ExplorerCSRF via postMessage (hög)2025

Mönstret är tydligt: majoriteten av bristerna handlar om just resursförbrukning och komplexitet, exakt de kategorier som depth limiting och kostnadsanalys i steg 4 och 5 är byggda för att stoppa. Auktoriseringsbristen i CVE-2025-64530 påminner om varför steg 8 inte går att hoppa över, även om du litar på din gateway.

Värt att notera är att samtliga tio sårbarheter i tabellen drabbade väletablerade, aktivt underhållna projekt, inte experimentella eller övergivna bibliotek. Apollo Router och Apollo Server hör till de mest använda GraphQL-servrarna i produktion globalt, och GitLab driver ett av de mest granskade open source-projekten som finns. Slutsatsen är att mogen kod inte är immun mot GraphQL-specifika designproblem, vilket är precis varför lagren i den här guiden behöver sättas upp explicit i din egen kodbas snarare än att antas komma på köpet från ramverket.

Ett annat mönster är hur snabbt kategorin skiftat. 2024 dominerades listan av renodlade DoS-brister, medan 2025 lade till både en kritisk CSRF-sårbarhet i utvecklarverktygen och den allvarliga auktoriseringsbristen i federationslagret. Håll koll på säkerhetsaviseringar för dina GraphQL-beroenden löpande, inte bara vid större versionsuppgraderingar.

Vanliga fallgropar när du säkrar GraphQL

  1. Att tro att djupbegränsning ensam stoppar allt. En bred fråga med många fält på samma nivå kostar lika mycket som en djup, men klarar sig förbi graphql-depth-limit helt obemärkt.
  2. Att bara stänga introspection i vissa miljöer. Om staging eller en intern testmiljö exponerar schemat publikt spelar produktionsinställningen ingen roll.
  3. Att sätta en hög kostnadsgräns “för säkerhets skull” utan att mäta verklig trafik. En gräns som aldrig triggas skyddar inte mot något.
  4. Att lägga all auktorisering i gatewayen. Som CVE-2025-64530 visade räcker det inte, resolvrar som hanterar känsliga fält behöver egna kontroller.
  5. Att tänka REST när du designar rate limiting. En rate limiter som räknar HTTP-anrop missar batching- och aliasattacker helt.
  6. Att rate-limita enbart per IP. Delade företagsnät och mobiloperatörers NAT gör IP till en dålig identifierare, kombinera med API-nyckel eller användar-id.
  7. Att inte logga avvisade förfrågningar. Utan loggning missar du att någon aktivt sonderar din API innan de hittar en verklig lucka.
  8. Att exponera stacktrace eller interna felmeddelanden i produktion. Apollo Server visar detaljerade fel som standard i vissa konfigurationer, vilket kan läcka filsökvägar, databasnamn och interna fältnamn till en angripare som medvetet skickar felaktiga frågor.

Felsökning: vanliga problem och lösningar

  • Problem: __schema-frågor fungerar fortfarande i produktion. Lösning: kontrollera att NODE_ENV=production faktiskt är satt i containern eller CI/CD-pipelinen, inte bara lokalt.
  • Problem: legitima frontend-frågor blockeras av djupgränsen. Lösning: logga faktiskt frågedjup i produktionstrafik i minst en vecka innan du låser ett värde, höj sedan gränsen stegvis.
  • Problem: en enkel fråga triggar felmeddelande om för hög komplexitet. Lösning: justera estimatorn så listor med paginering (till exempel first: 50) viktas efter faktiskt antal poster, inte som ett enda fält.
  • Problem: rate limitern blockerar ett helt kontor på en gång. Lösning: byt nyckel från ren IP till API-nyckel eller autentiserat användar-id.
  • Problem: klienten får PersistedQueryNotFound. Lösning: kontrollera att Apollo Client är konfigurerad att först skicka hash och falla tillbaka till full frågetext vid cache-miss.
  • Problem: auktoriseringsfelet dyker upp efter att en databasfråga redan körts. Lösning: flytta behörighetskontrollen till toppen av resolvern, före datahämtningen.
  • Problem: batchade förfrågningar verkar kringgå rate limitern. Lösning: räkna antal operationer i batchen, inte antal HTTP-anrop, och sätt ett separat tak på operationer per batch.
  • Problem: minnesanvändningen ökar under lasttest efter att kostnadsanalysen lades till. Lösning: flytta loggningen i onComplete till en asynkron kö i stället för synkron skrivning till disk.

Avancerade tips och verktygsjämförelse för produktion

Kostnadsbudget per användartier

I stället för en global kostnadsgräns för alla klienter, koppla budgeten till autentiserad identitet. En betalande kund eller ett internt system kan få en högre komplexitetsbudget per minut än en anonym eller gratisanvändare, vilket gör att du kan hålla en strikt standardgräns utan att straffa dina viktigaste integrationer.

Härdning i federerade arkitekturer

Om du kör Apollo Federation, applicera djup- och kostnadsgränser både i gatewayen och i varje underliggande subgraph. CVE-2025-32031 visade att en optimering i gatewayen kunde kringgås, vilket innebär att ett enda skyddslager i frontend-gatewayen inte är tillräckligt om en angripare hittar en väg direkt till en subgraph.

Andra åtgärder värda att överväga när trafiken växer: spåra enskilda frågor med OpenTelemetry för att upptäcka avvikande mönster automatiskt, kör schemadiff i CI så att ett känsligt fält aldrig av misstag exponeras via en pull request, och placera ett edge-lager eller en WAF framför GraphQL-endpointen som ett första filter mot uppenbart skadlig trafik innan den ens når applikationskoden.

Timeout, felmeddelanden och GET-baserade frågor

Sätt alltid en hård timeout på varje GraphQL-exekvering, till exempel fem sekunder, som ett sista skyddsnät om en fråga ändå slinker igenom djup- och kostnadsgränserna på grund av en resolver som gör en oväntat dyr extern anrop. Kombinera det med att slå av detaljerade felmeddelanden (includeStacktraceInErrorResponses: false i Apollo Server) i produktion, så att interna fältnamn och filvägar inte läcker till en klient som medvetet skickar trasiga frågor för att kartlägga systemet. Om du stödjer GraphQL-frågor via HTTP GET för cachning, se till att samma djup- och kostnadsgränser gäller där också, en vanlig miss är att bara skydda POST-vägen.

PaketSyfteSenaste version
graphql-depth-limitDjupbegränsning1.1.0
graphql-query-complexityKostnadsanalys per fält2.0.0
express-rate-limitHastighetsbegränsning8.6.2
graphql-armorSamlat säkerhetslager (tidig version, testa noggrant)0.0.3
@apollo/serverGraphQL-server5.5.1

Vanliga frågor

Vad är introspection i GraphQL och varför är det farligt i produktion?
Introspection är en inbyggd funktion som låter en klient fråga schemat om sina egna typer och fält. Praktiskt under utveckling, men i produktion ger det en angripare en fullständig karta över din datamodell utan att behöva gissa sig fram.

Räcker rate limiting för att stoppa DoS-attacker mot GraphQL?
Nej. Rate limiting skyddar mot volym av anrop, medan djup- och kostnadsanalys skyddar mot att ett enda anrop är för dyrt. Du behöver båda, vilket flera av Apollo Routers CVE:er under 2024–2025 visat tydligt.

Hur skiljer sig GraphQL-injektion från SQL-injektion?
Mekaniken är samma, argument som inte valideras eller parametriseras kan nå din databas obehandlade. Skillnaden är att GraphQLs typade schema ibland ger en falsk trygghetskänsla, trots att typning aldrig ersätter parametriserade databasfrågor.

Behöver jag persisted queries om jag redan har kostnadsanalys?
De löser olika problem. Kostnadsanalys begränsar hur dyr en tillåten fråga får vara, medan persisted queries som allowlist helt stoppar frågor som inte redan är kända och godkända, vilket är ett starkare skydd för publika mobilappar och webbklienter.

Fungerar de här skydden med Apollo Federation och gateway-arkitekturer?
Ja, men de måste appliceras på flera nivåer. Sätt gränser både i gatewayen och i varje subgraph, eftersom CVE-2025-32031 visade att en gateway-optimering gick att kringgå.

Hur ofta bör jag uppdatera paket som graphql-depth-limit och graphql-query-complexity?
Behandla dem som säkerhetskritisk kod, inte bara vanliga beroenden. Kör automatiserad sårbarhetsskanning i din CI-pipeline och prenumerera på säkerhetsaviseringar för dina GraphQL-relaterade paket.

Vilket verktyg ska jag välja för att testa min GraphQL-APIs säkerhet innan lansering?
Börja med OWASP:s GraphQL Cheat Sheet som checklista, komplettera med manuella tester av introspection, djup och batching genom Burp-tillägget InQL eller Postman, och lägg till en automatiserad GraphQL-medveten skanner i CI innan varje release mot produktion.

Måste jag skriva om hela min befintliga GraphQL-API för att lägga till de här skydden?
Nej. Alla tolv stegen i den här guiden går att lägga till stegvis ovanpå ett befintligt schema och en befintlig resolverstruktur, utan att ändra själva datamodellen. Ett rimligt tillvägagångssätt är att börja med steg 3 (introspection) och steg 4–5 (djup och kostnad), eftersom de ger mest skydd för minst kodändring, och sedan arbeta dig vidare mot auktorisering och persisted queries när du har mätt verklig produktionstrafik.