graphql-security

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

GraphQL Security Skill

GraphQL安全技能

Protect your GraphQL APIs from attacks
保护你的GraphQL API免受攻击

Overview

概述

Learn essential security patterns for GraphQL: JWT authentication, role-based authorization, rate limiting, query complexity limits, and input validation.

学习GraphQL的核心安全模式:JWT身份验证、基于角色的授权、速率限制、查询复杂度限制以及输入验证。

Security Checklist

安全检查清单

CheckPriorityImplementation
AuthenticationCriticalJWT with refresh tokens
AuthorizationCriticalField-level with graphql-shield
Rate LimitingCriticalPer-user/IP with Redis
Query DepthHighgraphql-depth-limit
Query ComplexityHighgraphql-query-complexity
IntrospectionHighDisable in production
Input ValidationHighValidate all inputs
Error MaskingMediumHide internal errors

检查项优先级实现方案
身份验证关键带刷新令牌的JWT
授权关键使用graphql-shield实现字段级控制
速率限制关键基于用户/IP结合Redis实现
查询深度限制使用graphql-depth-limit
查询复杂度限制使用graphql-query-complexity
自省查询生产环境中禁用
输入验证验证所有输入内容
错误掩码隐藏内部错误信息

Core Patterns

核心模式

1. JWT Authentication

1. JWT身份验证

typescript
import jwt from 'jsonwebtoken';

// Token creation
function createTokens(user) {
  const accessToken = jwt.sign(
    { userId: user.id, roles: user.roles },
    process.env.JWT_SECRET,
    { expiresIn: '15m' }
  );

  const refreshToken = jwt.sign(
    { userId: user.id },
    process.env.JWT_REFRESH_SECRET,
    { expiresIn: '7d' }
  );

  return { accessToken, refreshToken };
}

// Context setup
const context = async ({ req }) => {
  const token = req.headers.authorization?.replace('Bearer ', '');

  let user = null;
  if (token) {
    try {
      const payload = jwt.verify(token, process.env.JWT_SECRET);
      user = await db.users.findById(payload.userId);
    } catch (e) {
      // Token invalid or expired
    }
  }

  return { user };
};

// Login resolver
const resolvers = {
  Mutation: {
    login: async (_, { email, password }) => {
      const user = await db.users.findByEmail(email);

      if (!user || !await bcrypt.compare(password, user.passwordHash)) {
        throw new GraphQLError('Invalid credentials', {
          extensions: { code: 'UNAUTHORIZED' }
        });
      }

      return { ...createTokens(user), user };
    },
  },
};
typescript
import jwt from 'jsonwebtoken';

// Token creation
function createTokens(user) {
  const accessToken = jwt.sign(
    { userId: user.id, roles: user.roles },
    process.env.JWT_SECRET,
    { expiresIn: '15m' }
  );

  const refreshToken = jwt.sign(
    { userId: user.id },
    process.env.JWT_REFRESH_SECRET,
    { expiresIn: '7d' }
  );

  return { accessToken, refreshToken };
}

// Context setup
const context = async ({ req }) => {
  const token = req.headers.authorization?.replace('Bearer ', '');

  let user = null;
  if (token) {
    try {
      const payload = jwt.verify(token, process.env.JWT_SECRET);
      user = await db.users.findById(payload.userId);
    } catch (e) {
      // Token invalid or expired
    }
  }

  return { user };
};

// Login resolver
const resolvers = {
  Mutation: {
    login: async (_, { email, password }) => {
      const user = await db.users.findByEmail(email);

      if (!user || !await bcrypt.compare(password, user.passwordHash)) {
        throw new GraphQLError('Invalid credentials', {
          extensions: { code: 'UNAUTHORIZED' }
        });
      }

      return { ...createTokens(user), user };
    },
  },
};

2. Authorization with graphql-shield

2. 使用graphql-shield实现授权

typescript
import { rule, shield, and, or } from 'graphql-shield';

// Rules
const isAuthenticated = rule()((_, __, { user }) => user !== null);

const isAdmin = rule()((_, __, { user }) =>
  user?.roles?.includes('ADMIN')
);

const isOwner = rule()(async (_, { id }, { user, dataSources }) => {
  const resource = await dataSources.findById(id);
  return resource?.userId === user?.id;
});

// Permissions
const permissions = shield({
  Query: {
    me: isAuthenticated,
    users: and(isAuthenticated, isAdmin),
    user: and(isAuthenticated, or(isOwner, isAdmin)),
  },
  Mutation: {
    updateUser: and(isAuthenticated, or(isOwner, isAdmin)),
    deleteUser: and(isAuthenticated, isAdmin),
  },
  User: {
    email: or(isOwner, isAdmin),
    privateField: isOwner,
  },
}, {
  fallbackError: new GraphQLError('Not authorized'),
});

// Apply
import { applyMiddleware } from 'graphql-middleware';
const protectedSchema = applyMiddleware(schema, permissions);
typescript
import { rule, shield, and, or } from 'graphql-shield';

// Rules
const isAuthenticated = rule()((_, __, { user }) => user !== null);

const isAdmin = rule()((_, __, { user }) =>
  user?.roles?.includes('ADMIN')
);

const isOwner = rule()(async (_, { id }, { user, dataSources }) => {
  const resource = await dataSources.findById(id);
  return resource?.userId === user?.id;
});

// Permissions
const permissions = shield({
  Query: {
    me: isAuthenticated,
    users: and(isAuthenticated, isAdmin),
    user: and(isAuthenticated, or(isOwner, isAdmin)),
  },
  Mutation: {
    updateUser: and(isAuthenticated, or(isOwner, isAdmin)),
    deleteUser: and(isAuthenticated, isAdmin),
  },
  User: {
    email: or(isOwner, isAdmin),
    privateField: isOwner,
  },
}, {
  fallbackError: new GraphQLError('Not authorized'),
});

// Apply
import { applyMiddleware } from 'graphql-middleware';
const protectedSchema = applyMiddleware(schema, permissions);

3. Rate Limiting

3. 速率限制

typescript
// Express-level (basic)
import rateLimit from 'express-rate-limit';

app.use('/graphql', rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,
  keyGenerator: (req) => req.user?.id || req.ip,
}));

// GraphQL-level (granular)
const typeDefs = gql`
  directive @rateLimit(max: Int!, window: String!) on FIELD_DEFINITION

  type Mutation {
    login(email: String!, password: String!): AuthPayload!
      @rateLimit(max: 5, window: "15m")

    sendEmail(input: SendEmailInput!): Boolean!
      @rateLimit(max: 10, window: "1h")
  }
`;
typescript
// Express-level (basic)
import rateLimit from 'express-rate-limit';

app.use('/graphql', rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,
  keyGenerator: (req) => req.user?.id || req.ip,
}));

// GraphQL-level (granular)
const typeDefs = gql`
  directive @rateLimit(max: Int!, window: String!) on FIELD_DEFINITION

  type Mutation {
    login(email: String!, password: String!): AuthPayload!
      @rateLimit(max: 5, window: "15m")

    sendEmail(input: SendEmailInput!): Boolean!
      @rateLimit(max: 10, window: "1h")
  }
`;

4. Query Limits

4. 查询限制

typescript
import depthLimit from 'graphql-depth-limit';
import { createComplexityLimitRule } from 'graphql-validation-complexity';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [
    // Max depth of 10
    depthLimit(10),

    // Max complexity of 1000
    createComplexityLimitRule(1000, {
      scalarCost: 1,
      objectCost: 2,
      listFactor: 10,
    }),
  ],

  // Disable introspection in production
  introspection: process.env.NODE_ENV !== 'production',
});
typescript
import depthLimit from 'graphql-depth-limit';
import { createComplexityLimitRule } from 'graphql-validation-complexity';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [
    // Max depth of 10
    depthLimit(10),

    // Max complexity of 1000
    createComplexityLimitRule(1000, {
      scalarCost: 1,
      objectCost: 2,
      listFactor: 10,
    }),
  ],

  // Disable introspection in production
  introspection: process.env.NODE_ENV !== 'production',
});

5. Input Validation

5. 输入验证

typescript
import validator from 'validator';
import xss from 'xss';

const validate = {
  email: (v) => {
    if (!validator.isEmail(v)) throw new Error('Invalid email');
    return validator.normalizeEmail(v);
  },

  password: (v) => {
    if (v.length < 8) throw new Error('Password too short');
    if (!/[A-Z]/.test(v)) throw new Error('Need uppercase');
    if (!/[0-9]/.test(v)) throw new Error('Need number');
    return v;
  },

  html: (v) => xss(v),
};

const resolvers = {
  Mutation: {
    createUser: async (_, { input }) => {
      const clean = {
        email: validate.email(input.email),
        password: validate.password(input.password),
        bio: input.bio ? validate.html(input.bio) : null,
      };
      return db.users.create(clean);
    },
  },
};
typescript
import validator from 'validator';
import xss from 'xss';

const validate = {
  email: (v) => {
    if (!validator.isEmail(v)) throw new Error('Invalid email');
    return validator.normalizeEmail(v);
  },

  password: (v) => {
    if (v.length < 8) throw new Error('Password too short');
    if (!/[A-Z]/.test(v)) throw new Error('Need uppercase');
    if (!/[0-9]/.test(v)) throw new Error('Need number');
    return v;
  },

  html: (v) => xss(v),
};

const resolvers = {
  Mutation: {
    createUser: async (_, { input }) => {
      const clean = {
        email: validate.email(input.email),
        password: validate.password(input.password),
        bio: input.bio ? validate.html(input.bio) : null,
      };
      return db.users.create(clean);
    },
  },
};

6. Error Masking

6. 错误掩码

typescript
const server = new ApolloServer({
  formatError: (error) => {
    // Log full error
    console.error(error);

    // In production, hide internal errors
    if (process.env.NODE_ENV === 'production') {
      if (error.extensions?.code === 'INTERNAL_SERVER_ERROR') {
        return { message: 'Internal error', extensions: { code: 'INTERNAL_ERROR' } };
      }
    }

    return error;
  },
});

typescript
const server = new ApolloServer({
  formatError: (error) => {
    // Log full error
    console.error(error);

    // In production, hide internal errors
    if (process.env.NODE_ENV === 'production') {
      if (error.extensions?.code === 'INTERNAL_SERVER_ERROR') {
        return { message: 'Internal error', extensions: { code: 'INTERNAL_ERROR' } };
      }
    }

    return error;
  },
});

Security Headers

安全头部配置

typescript
import helmet from 'helmet';
import cors from 'cors';

app.use(helmet());
app.use(cors({
  origin: process.env.ALLOWED_ORIGINS?.split(','),
  credentials: true,
}));
app.use(express.json({ limit: '100kb' }));

typescript
import helmet from 'helmet';
import cors from 'cors';

app.use(helmet());
app.use(cors({
  origin: process.env.ALLOWED_ORIGINS?.split(','),
  credentials: true,
}));
app.use(express.json({ limit: '100kb' }));

Troubleshooting

故障排查

IssueCauseSolution
Token always invalidClock skewAdd grace period
Rate limit bypassWrong keyUse user ID when authenticated
Auth not workingContext asyncAwait context setup
Introspection exposedWrong env checkVerify NODE_ENV
问题原因解决方案
令牌始终无效时钟偏差添加宽限期
速率限制被绕过键值错误已认证时使用用户ID作为标识
身份验证不生效上下文未异步处理等待上下文初始化完成
自省查询暴露环境变量检查错误验证NODE_ENV配置

Security Testing

安全测试

bash
undefined
bash
undefined

Test introspection (should fail in prod)

Test introspection (should fail in prod)

curl -X POST $API
-H "Content-Type: application/json"
-d '{"query":"{ __schema { types { name } } }"}'
curl -X POST $API
-H "Content-Type: application/json"
-d '{"query":"{ __schema { types { name } } }"}'

Test rate limit

Test rate limit

for i in {1..20}; do curl -X POST $API
-d '{"query":"mutation { login(email:"x",password:"y") { token } }"}' done
for i in {1..20}; do curl -X POST $API
-d '{"query":"mutation { login(email:"x",password:"y") { token } }"}' done

Test depth limit (should fail)

Test depth limit (should fail)

curl -X POST $API
-d '{"query":"{ user { posts { author { posts { author { id } } } } } }"}'

---
curl -X POST $API
-d '{"query":"{ user { posts { author { posts { author { id } } } } } }"}'

---

Usage

使用方法

Skill("graphql-security")
Skill("graphql-security")

Related Skills

相关技能

  • graphql-apollo-server
    - Server configuration
  • graphql-resolvers
    - Auth in resolvers
  • graphql-schema-design
    - Auth-aware schema
  • graphql-apollo-server
    - 服务器配置
  • graphql-resolvers
    - 解析器中的身份验证
  • graphql-schema-design
    - 支持身份验证的Schema设计

Related Agent

相关Agent

  • 06-graphql-security
    - For detailed guidance
  • 06-graphql-security
    - 提供详细指导