Привет, Хабр! Представляю вашему вниманию перевод статьи Protecting Your GraphQL API From Security Vulnerabilities.
GraphQL быстро становится выбором разработчиков, которым необходимо создать API для своего клиентского приложения. Но, как и все новые технологии, GraphQL подвержен некоторым присущим ему угрозам безопасности. Независимо от того, создаете ли вы сторонний проект или крупномасштабное корпоративное приложение, вам необходимо убедиться, что вы защищаете себя от этих угроз.
Хотя угрозы, перечисленные в этом посте, относятся непосредственно к GraphQL, ваша реализация представит новый набор угроз, которые необходимо будет устранить. Также важно, чтобы вы понимали угрозы, которым подвержены любые приложения, работающие в сети.
Угроза: большие, глубоко вложенные запросы, которые дорого вычислять
Решение: ограничение глубины вложенности
Мощь, предоставляемая GraphQL, связана с некоторыми новыми угрозами безопасности. Наиболее распространенными являются глубоко вложенные запросы, которые приводят к дорогостоящим вычислениям и огромным JSON, которые могут негативно повлиять на сеть и ее пропускную способность.
Правильный способ защитить вашe API от такого рода атак — это ограничить глубину запросов, чтобы вредоносные глубокие запросы блокировались до вычисления результата.
GraphQL Depth Limit предоставляет простой интерфейс для ограничения глубины запросов.
import depthLimit from 'graphql-depth-limit'
import express from 'express'
import graphqlHTTP from 'express-graphql'
import schema from './schema'
const app = express()
app.use('/graphql', graphqlHTTP((req, res) => ({
schema,
validationRules: [ depthLimit(10) ]
})))
Угроза: перебор уязвимых mutation-запросов
Решение: ограничение количества запросов
Перебор логинов и паролей — самый старый трюк в истории взломов. В прошлом десятилетии в интернете произошло так много утечек данных, что недавно была найдена база из 772 904 991 уникальных электронных почт и 21 222 975 уникальных паролей. Для проверки, не утекла ли информация о ваших почте и пароле, Трой Хант даже создал сайт Have I been Pwned, для которого, числе прочих, использовал эту базу.
К счастью, у вас есть простой способ сделать перебор действительно трудным и затратным для злоумышленников, что сделает вас для них менее привлекательной целью.
Плагин GraphQL Rate Limit позволяет указывать ограничения для ваших запросов тремя различными способами: с помощью пользовательских директив graphql-shield или с помощью функции ограничения базового количества запросов.
Этот плагин позволят установить временное окно и ограничение на количество запросов для него. Установка большого временного окна для очень уязвимых запросов, таких как вход в систему, и небольших временных окон для менее уязвимых запросов поможет вам поддерживать приятный опыт для обычных пользователей и станет кошмаром для злоумышленников.
Создайте директиву для ограничения количества запросов:
Здесь понадобится уникальный идентификатор для каждого запроса. Вы можете использовать IP-адрес пользователя или другой идентификатор, который уникален для каждого пользователя и соответствует каждому запросу.
const rateLimitDirective = createRateLimitDirective({
identifyContext: (context) => {
return context.id
},
})
Добавьте директиву в вашу схему:
import { createRateLimitDirective } from 'graphql-rate-limit'
export const schema = {
typeDefs,
resolvers,
schemaDirectives: {
rateLimit: rateLimitDirective,
},
}
export default schema
Наконец, добавьте директиву к вашему уязвимому запросу:
# Максимальное число запросов ограничено десятью штуками в 60 секунд
Login(input: LoginInput!): User
@rateLimit(
window: "60s"
max: 10
message: "You are doing that too often. Please wait 60 seconds before trying again."
)
Угроза: разрешение пользователю влиять на специфичные для конкретного пользователя данные
Решение: брать эти данные из пользовательской сессии, где это возможно
Легко считать, что если вы хотите позволить пользователю обновлять какой-либо ресурс, то стоит давать пользователю самому решать, какой именно ресурс он хочет обновить. Но что, если пользователь получит идентификатор ресурса, к которому на самом деле у него не должно быть доступа?
Допустим, у нас есть mutation-запрос UpdateUser, который позволяет пользователю обновлять свой профиль.
mutation UpdateUser($input: {"id": "test123" , "email": "test@example.com"}) {
UpdateUser(input: $input) {
id
firstName
lastName
}
}
Если на стороне сервера не будет какой-либо защиты, то злоумышленник, имея список идентификаторов, потенциально сможет обновить адрес электронной почты для любого пользователя. Очевидное решение здесь — добавить проверку, чтобы убедиться, что идентификатор текущего пользователя совпадает с идентификатором в полях ввода.
Не делайте так:
function updateUser({ id, email }) {
return User.findOneAndUpdate({ _id: id }, { email })
.catch(error => {
throw error;
});
}
Менее очевидный, но правильный способ решения этой проблемы — не допускать использование идентификатора в качестве входных данных и использовать идентификатор пользователя из объекта контекста.
Делайте так:
function updateUser({ email }, context) {
return User.findOneAndUpdate({ _id: context.user._id }, { email })
.catch(error => {
throw error;
});
}
Возможно, это довольно тривиальный пример, но выполнение подобных действий для каждого из объектов, с которыми напрямую взаимодействует пользователь, может защитить вас от множества рискованных ошибок.
Выполнение нескольких дорогих запросов одновременно
Решение: ограничение стоимости запросов
Назначая цену каждому запросу и указав максимальную цену за запрос, мы можем защитить себя от злоумышленников, которые могут попытаться выполнить слишком много дорогих запросов одновременно.
Плагин GraphQL Cost Analysis — это простой способ указать стоимость для запросов и ограничение на максимальную стоимость.
Определите максимальную стоимость:
app.use(
'/graphql',
graphqlExpress(req => {
return {
schema,
rootValue: null,
validationRules: [
costAnalysis({
variables: req.body.variables,
maximumCost: 1000,
}),
],
}
})
)
Определите стоимость для каждого запроса:
Query: {
Article: {
multipliers: ['limit'],
useMultipliers: true,
complexity: 3,
},
}
Угроза: раскрытие деталей реализации GraphQL
Решение: отключить интроспекцию в "боевом" коде
Среда GraphQL — чрезвычайно полезный инструмент для разработки. Он настолько мощный, что даже документирует вашу схему, запросы и подписки за вас. Эта информация может стать золотой жилой для злоумышленников, желающих найти уязвимости в вашем приложении.
Плагин GraphQL Display Introspection предотвратит утечку вашей схемы в открытый доступ. Просто импортируйте плагин и примените его к своим правилам валидации.
import express from 'express';
import bodyParser from 'body-parser';
import { graphqlExpress } from 'graphql-server-express';
+ import NoIntrospection from 'graphql-disable-introspection';
const myGraphQLSchema = // ... определите здесь вашу схему!
const PORT = 3000;
var app = express();
app.use('/graphql', bodyParser.json(), graphqlExpress({
schema: myGraphQLSchema,
validationRules: [NoIntrospection]
}));
app.listen(PORT);