🚀 Nuova versione beta disponibile! Feedback o problemi? Contattaci

Sicurezza delle Query in GraphQL: Best Practices per Proteggere le API

Codegrind Team•Sep 04 2024

Le API GraphQL offrono flessibilità e potenza nel recupero dei dati, ma con questa flessibilità emergono anche sfide legate alla sicurezza. Senza adeguate misure di protezione, le query maligne o l’abuso delle API possono portare a problemi di performance, vulnerabilità e attacchi come Denial of Service (DoS). In questo articolo esploreremo le migliori pratiche per garantire la sicurezza delle query in GraphQL e prevenire attacchi, mantenendo le API sicure e performanti.

Principali Minacce alla Sicurezza delle API GraphQL

  1. Attacchi Denial of Service (DoS): Query eccessivamente complesse o nidificate possono sovraccaricare il server, causando rallentamenti o blocchi.
  2. Injection: Le query GraphQL possono essere vulnerabili agli attacchi di injection se non vengono gestiti correttamente gli input.
  3. Abuso delle API: L’eccessiva flessibilità di GraphQL può essere sfruttata per abusare delle API, richiedendo grandi quantità di dati in una sola richiesta.
  4. Esposizione di Dati Sensibili: Se non vengono implementati controlli di autorizzazione adeguati, i client possono accedere a dati non autorizzati.

Best Practices per la Sicurezza delle Query in GraphQL

1. Limitare la Complessità delle Query

Uno degli attacchi più comuni contro GraphQL è l’invio di query molto complesse o profondamente nidificate, che possono mettere sotto pressione il server. Limitare la complessità delle query è fondamentale per prevenire attacchi DoS.

Soluzione: Analisi della Complessità delle Query

Puoi utilizzare librerie come graphql-query-complexity per valutare e limitare la complessità delle query eseguite dai client.

Esempio di Implementazione

npm install graphql-query-complexity
const { getComplexity, simpleEstimator } = require("graphql-query-complexity");

const server = new ApolloServer({
  schema,
  plugins: [
    {
      requestDidStart: () => ({
        didResolveOperation({ request, document }) {
          const complexity = getComplexity({
            schema,
            query: document,
            variables: request.variables,
            estimators: [simpleEstimator({ defaultComplexity: 1 })],
          });

          if (complexity > 100) {
            throw new Error(
              `La complessità della query è troppo alta: ${complexity}`
            );
          }
        },
      }),
    },
  ],
});

In questo esempio, il sistema analizza la complessità delle query e blocca quelle che superano una soglia di 100.

2. Limitare la Profondità delle Query Nidificate

Le query nidificate in GraphQL possono crescere rapidamente in profondità, creando un carico enorme sul server. Questo può essere sfruttato per attacchi DoS.

Soluzione: Limitare la Profondità

È possibile limitare la profondità delle query per prevenire un uso eccessivo di query nidificate.

Esempio di Implementazione con graphql-depth-limit

npm install graphql-depth-limit
const depthLimit = require("graphql-depth-limit");

const server = new ApolloServer({
  schema,
  validationRules: [depthLimit(5)], // Limita la profondità della query a 5 livelli
});

In questo esempio, la profondità delle query viene limitata a un massimo di 5 livelli.

3. Implementare il Rate Limiting

Per prevenire attacchi DoS e abuso delle API, è importante limitare il numero di richieste che un singolo client può effettuare in un determinato periodo di tempo.

Soluzione: Rate Limiting

Utilizza un middleware di rate limiting per impedire che un client invii troppe query in un breve lasso di tempo.

Esempio di Implementazione con express-rate-limit

npm install express-rate-limit
const rateLimit = require("express-rate-limit");

// Imposta il rate limit
const limiter = rateLimit({
  windowMs: 1 * 60 * 1000, // 1 minuto
  max: 100, // Limita ogni IP a 100 richieste al minuto
});

app.use("/graphql", limiter);

In questo esempio, un client può inviare al massimo 100 richieste al server GraphQL in un minuto.

4. Validazione e Sanitizzazione degli Input

Le injection (come l’SQL injection o altre forme di injection di codice) sono rischi comuni in qualsiasi applicazione che accetti input dal client. È essenziale validare e sanitizzare correttamente tutti gli input forniti dal client nelle query e mutations.

Soluzione: Validare gli Input

Implementa la validazione degli input in tutte le mutations e query, utilizzando librerie come Joi o validator.

Esempio di Validazione degli Input

npm install joi
const Joi = require("joi");

const resolvers = {
  Mutation: {
    createUser: (parent, { input }, context) => {
      const schema = Joi.object({
        name: Joi.string().min(3).max(30).required(),
        email: Joi.string().email().required(),
      });

      const { error } = schema.validate(input);
      if (error) {
        throw new Error("Input non valido");
      }

      // Crea l'utente dopo la validazione
      return context.db.createUser(input);
    },
  },
};

In questo esempio, l’input della mutation createUser viene validato utilizzando Joi, garantendo che name e email siano validi.

5. Autenticazione e Autorizzazione

L’autenticazione e l’autorizzazione sono fondamentali per proteggere i dati sensibili e garantire che solo gli utenti autorizzati possano accedere a determinate risorse.

Autenticazione con JSON Web Tokens (JWT)

Un approccio comune è l’uso di JWT (JSON Web Tokens) per autenticare i client. Il token viene inviato con ogni richiesta e il server lo verifica prima di eseguire le query o mutations.

Esempio di Implementazione con JWT

npm install jsonwebtoken
const jwt = require("jsonwebtoken");

const context = ({ req }) => {
  const token = req.headers.authorization || "";

  try {
    const user = jwt.verify(token, "secret_key");
    return { user };
  } catch (err) {
    throw new AuthenticationError("Token non valido");
  }
};

const server = new ApolloServer({
  schema,
  resolvers,
  context,
});

Autorizzazione nei Risolutori

Una volta autenticato l’utente, puoi gestire l’autorizzazione nei risolutori, garantendo che solo gli utenti con i permessi corretti possano accedere a determinate query o campi.

Esempio di Autorizzazione

const resolvers = {
  Query: {
    sensitiveData: (parent, args, context) => {
      if (!context.user || context.user.role !== "admin") {
        throw new ForbiddenError(
          "Non hai i permessi per accedere a questa risorsa"
        );
      }
      return getSensitiveData();
    },
  },
};

6. Query Whitelisting

Invece di permettere ai client di eseguire query dinamiche, puoi implementare un sistema di whitelisting delle query, dove solo un insieme predefinito di query è consentito.

Soluzione: Persisted Queries con Whitelisting

Le Persisted Queries consentono di memorizzare e autorizzare solo query approvate. I client possono inviare un identificatore di query predefinito, invece di inviare query dinamiche.

Esempio di Persisted Queries con Apollo Client

import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client";
import { createPersistedQueryLink } from "apollo-link-persisted-queries";

const link = createPersistedQueryLink().concat(
  new HttpLink({ uri: "http://localhost:4000/graphql" })
);

const client = new ApolloClient({
  cache: new InMemoryCache(),
  link,
});

In questo esempio, il client utilizza Persisted Queries per inviare solo query predefinite, riducendo il rischio di attacchi legati a query dinamiche.

7. Mascheramento degli Errori

In caso di errore durante l’esecuzione di una query, è importante evitare di esporre troppi dettagli interni del server. Fornire messaggi di errore generici previene la divulgazione di informazioni sensibili, che potrebbero essere utilizzate dagli attaccanti.

Soluzione: Mascheramento degli Errori

Maschera i dettagli degli errori rest

ituendo messaggi generici ai client.

const server = new ApolloServer({
  schema,
  formatError: (err) => {
    if (process.env.NODE_ENV === "production") {
      return new Error("Si è verificato un errore interno");
    }
    return err;
  },
});

In questo esempio, i dettagli completi degli errori vengono restituiti solo in ambienti di sviluppo.

Conclusione

La sicurezza delle query in GraphQL è fondamentale per garantire che le tue API siano protette da abusi, attacchi DoS e altre vulnerabilità. Implementando limitazioni sulla complessità e profondità delle query, autenticazione e autorizzazione robuste, validazione degli input e altre best practices, puoi proteggere le tue API GraphQL e fornire un servizio sicuro e affidabile ai tuoi utenti.