Sicurezza delle Query in GraphQL: Best Practices per Proteggere le API
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
- Attacchi Denial of Service (DoS): Query eccessivamente complesse o nidificate possono sovraccaricare il server, causando rallentamenti o blocchi.
- Injection: Le query GraphQL possono essere vulnerabili agli attacchi di injection se non vengono gestiti correttamente gli input.
- Abuso delle API: L’eccessiva flessibilità di GraphQL può essere sfruttata per abusare delle API, richiedendo grandi quantità di dati in una sola richiesta.
- 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.