Security is critical for production GraphQL APIs. This section covers essential security practices and common vulnerabilities.
- Security Overview
- Input Validation
- Output Sanitization
- Authentication Security
- Authorization Best Practices
- Rate Limiting
- Query Complexity Limiting
- CORS Configuration
- Preventing Common Attacks
- SQL Injection
- XSS (Cross-Site Scripting)
- CSRF
- Information Disclosure
- SSL/TLS Configuration
- Security Headers
- Logging and Monitoring
- Security Checklist
By the end of this section, you will:
- Implement proper input validation
- Secure authentication and authorization
- Prevent common web vulnerabilities
- Configure rate limiting
- Set up security monitoring
- Follow production security best practices
# Nginx configuration
server {
listen 443 ssl http2;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
}<?php
public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null)
{
// Validate required fields
if (!isset($args['email']) || empty($args['email'])) {
throw new GraphQlInputException(__('Email is required'));
}
// Validate email format
if (!filter_var($args['email'], FILTER_VALIDATE_EMAIL)) {
throw new GraphQlInputException(__('Invalid email format'));
}
// Validate input length
if (strlen($args['name']) > 255) {
throw new GraphQlInputException(__('Name is too long'));
}
// Sanitize input
$email = filter_var($args['email'], FILTER_SANITIZE_EMAIL);
// Continue processing...
}<?php
public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null)
{
// Check authentication
if (!$context->getUserId()) {
throw new GraphQlAuthenticationException(
__('Current user is not authenticated')
);
}
// Check authorization for specific resource
$customerId = $context->getUserId();
$requestedId = $args['customer_id'];
if ($customerId != $requestedId) {
throw new GraphQlAuthorizationException(
__('You are not authorized to access this resource')
);
}
// Continue processing...
}<!-- etc/di.xml -->
<type name="Magento\GraphQl\Controller\GraphQl">
<plugin name="rate_limiter" type="Vendor\Module\Plugin\RateLimiter"/>
</type><?php
namespace Vendor\Module\Plugin;
class RateLimiter
{
private $cache;
private const RATE_LIMIT = 100; // requests per minute
private const RATE_PERIOD = 60; // seconds
public function beforeDispatch(
\Magento\GraphQl\Controller\GraphQl $subject,
\Magento\Framework\App\RequestInterface $request
) {
$identifier = $this->getClientIdentifier($request);
$cacheKey = 'graphql_rate_limit_' . $identifier;
$requestCount = $this->cache->load($cacheKey) ?: 0;
if ($requestCount >= self::RATE_LIMIT) {
throw new \Magento\Framework\Exception\LocalizedException(
__('Rate limit exceeded. Please try again later.')
);
}
$this->cache->save($requestCount + 1, $cacheKey, [], self::RATE_PERIOD);
}
private function getClientIdentifier($request): string
{
// Use IP address or customer ID
return $request->getClientIp();
}
}<?php
namespace Vendor\Module\Plugin;
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
class QueryComplexityValidator
{
private const MAX_QUERY_DEPTH = 10;
private const MAX_QUERY_COMPLEXITY = 100;
public function beforeDispatch($subject, $request)
{
$query = $this->getQueryFromRequest($request);
if ($this->getQueryDepth($query) > self::MAX_QUERY_DEPTH) {
throw new GraphQlInputException(
__('Query is too deep. Maximum depth is %1', self::MAX_QUERY_DEPTH)
);
}
if ($this->getQueryComplexity($query) > self::MAX_QUERY_COMPLEXITY) {
throw new GraphQlInputException(
__('Query is too complex')
);
}
}
}<?php
public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null)
{
$data = $this->dataProvider->getData();
// Sanitize output to prevent XSS
return [
'name' => htmlspecialchars($data->getName(), ENT_QUOTES, 'UTF-8'),
'description' => strip_tags($data->getDescription()),
'email' => filter_var($data->getEmail(), FILTER_SANITIZE_EMAIL)
];
}<?php
// BAD - Exposes sensitive information
public function resolve(...)
{
return [
'customer' => [
'password_hash' => $customer->getPasswordHash(), // NEVER DO THIS
'api_key' => $customer->getApiKey(), // NEVER DO THIS
]
];
}
// GOOD - Only expose necessary data
public function resolve(...)
{
return [
'customer' => [
'firstname' => $customer->getFirstname(),
'lastname' => $customer->getLastname(),
'email' => $customer->getEmail()
]
];
}<?php
// BAD - Vulnerable to SQL injection
$sql = "SELECT * FROM products WHERE sku = '" . $args['sku'] . "'";
// GOOD - Use prepared statements or repositories
$searchCriteria = $this->searchCriteriaBuilder
->addFilter('sku', $args['sku'], 'eq')
->create();
$products = $this->productRepository->getList($searchCriteria);<!-- etc/di.xml -->
<type name="Magento\GraphQl\Controller\GraphQl">
<plugin name="cors_headers" type="Vendor\Module\Plugin\CorsHeaders"/>
</type><?php
namespace Vendor\Module\Plugin;
class CorsHeaders
{
public function afterDispatch($subject, $result, $request)
{
$response = $result;
// Only allow specific origins
$allowedOrigins = [
'https://your-frontend.com',
'https://your-app.com'
];
$origin = $request->getHeader('Origin');
if (in_array($origin, $allowedOrigins)) {
$response->setHeader('Access-Control-Allow-Origin', $origin);
$response->setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
$response->setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
$response->setHeader('Access-Control-Max-Age', '86400');
}
return $response;
}
}<?php
private function logSecurityEvent($event, $context = [])
{
$this->logger->warning('GraphQL Security Event: ' . $event, [
'ip' => $this->request->getClientIp(),
'user_id' => $this->context->getUserId(),
'timestamp' => time(),
'context' => $context
]);
}
// Usage
if (!$this->isAuthorized()) {
$this->logSecurityEvent('Unauthorized access attempt', [
'resource' => $resourceId,
'action' => 'read'
]);
throw new GraphQlAuthorizationException(__('Not authorized'));
}- ✅ Use HTTPS only in production
- ✅ Implement proper token-based authentication
- ✅ Set appropriate token expiration
- ✅ Validate user permissions for all protected resources
- ✅ Never trust client input
- ✅ Implement role-based access control
- ✅ Validate all inputs
- ✅ Use type validation
- ✅ Implement length limits
- ✅ Sanitize user input
- ✅ Validate email formats
- ✅ Check for required fields
- ✅ Implement rate limiting per IP/user
- ✅ Limit query depth
- ✅ Limit query complexity
- ✅ Set maximum batch size
- ✅ Implement timeout limits
- ✅ Never expose password hashes
- ✅ Never expose API keys
- ✅ Filter sensitive data from responses
- ✅ Sanitize output to prevent XSS
- ✅ Use parameterized queries
- ✅ Encrypt sensitive data at rest
- ✅ Log authentication attempts
- ✅ Log authorization failures
- ✅ Monitor for suspicious patterns
- ✅ Set up alerts for security events
- ✅ Regular security audits
- ✅ Monitor query performance
- ✅ Use Web Application Firewall (WAF)
- ✅ Enable DDoS protection
- ✅ Keep Magento and dependencies updated
- ✅ Regular security patches
- ✅ Secure server configuration
- ✅ Database access restrictions
// BAD - Weak password validation
if (strlen($password) >= 6) {
// Create account
}
// GOOD - Strong password requirements
if (!preg_match('/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/', $password)) {
throw new GraphQlInputException(__('Password must be at least 8 characters with uppercase, lowercase, number, and special character'));
}// BAD - Exposes internal details
catch (\Exception $e) {
throw new GraphQlInputException(__($e->getMessage()));
}
// GOOD - Generic error message, log details
catch (\Exception $e) {
$this->logger->error($e->getMessage());
throw new GraphQlInputException(__('An error occurred processing your request'));
}// BAD - No logging
if (!$this->authorize($user, $resource)) {
throw new GraphQlAuthorizationException(__('Not authorized'));
}
// GOOD - Log security events
if (!$this->authorize($user, $resource)) {
$this->securityLogger->warning('Authorization failed', [
'user_id' => $user->getId(),
'resource' => $resource,
'ip' => $this->getClientIp()
]);
throw new GraphQlAuthorizationException(__('Not authorized'));
}Start with Security Overview for a comprehensive security introduction.