Firebase Auth自定义JWT篡改风险及权限验证最佳实践咨询
Firebase Auth自定义声明篡改的安全问题与解决方案
问题背景
我基于Firebase v2版onCall云函数开发无服务器后端,编写了中间件实现认证与基于角色的访问控制(RBAC),部分API要求用户令牌包含roles: ["admin", "superadmin"]自定义声明。但测试发现,用户可篡改JWT令牌中的roles属性,从而绕过权限控制获取管理员权限。
复现步骤
- 用户通过邮箱密码登录Firebase Auth,获取包含
roles: ["basic"]的Bearer令牌 - 解码令牌,将
roles改为["admin"]后重新编码 - 在Postman中使用篡改后的令牌调用API,成功获得管理员权限
现有中间件代码
export const withAuthentication = ( requiredAccessRoles?: string[] // Roles are optional ) => ( handler: (request: CallableRequest) => Promise<any> // Function to handle the request ): ((request: CallableRequest) => Promise<any>) => { return async (request) => { const authHeader = request.rawRequest.headers.authorization as | string | undefined; // Check if Authorization header is present and properly formatted if (!authHeader || !authHeader.startsWith("Bearer ")) throw new HttpsError("unauthenticated", "Invalid or missing ID token."); if (!request.auth || !request.auth.uid) throw new HttpsError("unauthenticated", "Unauthenticated user"); // Extract the ID token from the header const idToken = authHeader.slice(7).trim(); try { // Validate the ID token await getAuth().verifyIdToken(idToken); } catch (error) { throw new HttpsError("unauthenticated", "Invalid or expired ID token."); } // If roles are required, perform the role check if (requiredAccessRoles && requiredAccessRoles.length > 0) { // Check if the user has at least one of the required roles const hasAnyRole = requiredAccessRoles.some((role) => request.auth?.token.roles.includes(role) ); if (!hasAnyRole) throw new HttpsError("permission-denied", "Insufficient privileges"); } // Proceed with the original handler return handler(request); }; };
云函数示例代码
export const testApiEndpoint = onCall( withAuthentication(["admin", "superadmin"])(async (request) => { try { return { message: "access granted" }; } catch (error) { throw new HttpsError("internal", "Something went wrong"); } }) );
问题解答
1. 如何防范篡改令牌提权的安全漏洞?
你的核心问题是错误依赖了未经过服务端可信验证的request.auth.token.roles,同时中间件逻辑存在冗余验证导致的逻辑漏洞:
- 对于Firebase onCall云函数,平台会自动验证请求中的ID令牌,仅当令牌有效时才会填充
request.auth,因此request.auth本身是可信的。但你手动提取令牌并验证后,仍使用request.auth.token(可能来自客户端篡改后的令牌)来检查角色,这是风险点。 - 若你选择手动验证令牌,必须使用
verifyIdToken返回的经过签名验证的解码令牌来获取自定义声明,而非客户端传递的原始数据。
修改后的中间件代码:
export const withAuthentication = (requiredAccessRoles?: string[]) => (handler: (request: CallableRequest) => Promise<any>): ((request: CallableRequest) => Promise<any>) => { return async (request) => { // 依赖Firebase onCall自动验证的可信auth数据 if (!request.auth || !request.auth.uid) { throw new HttpsError("unauthenticated", "Unauthenticated user"); } // 角色校验使用可信的request.auth.token(由Firebase验证后填充) if (requiredAccessRoles && requiredAccessRoles.length > 0) { const userRoles = request.auth.token.roles as string[] || []; const hasAnyRole = requiredAccessRoles.some(role => userRoles.includes(role)); if (!hasAnyRole) { throw new HttpsError("permission-denied", "Insufficient privileges"); } } return handler(request); }; };
若你需要手动验证令牌(比如非onCall场景),则必须使用验证后的解码令牌:
// 手动验证场景下的角色校验逻辑 const decodedToken = await getAuth().verifyIdToken(idToken); const userRoles = decodedToken.roles as string[] || []; const hasAnyRole = requiredAccessRoles.some(role => userRoles.includes(role));
2. Firebase Auth中安全验证自定义声明角色的最佳实践
- 依赖Firebase原生验证机制:对于onCall/onRequest云函数,优先使用平台自动填充的
request.auth数据,避免手动提取令牌重复验证,减少逻辑漏洞。 - 始终使用服务端验证后的声明:若手动验证令牌,必须从
verifyIdToken返回的解码对象中获取自定义声明,绝对不能使用客户端传递的未验证数据。 - 最小权限原则:仅为用户分配必要的角色,避免过度授权;敏感操作(如修改角色)仅允许超级管理员通过安全的服务端逻辑执行,禁止客户端直接修改自定义声明。
- 定期刷新令牌:客户端使用
getIdToken(true)强制刷新令牌,确保自定义声明变更后能及时同步到客户端,避免用户使用过期的旧令牌。 - 日志与监控:记录角色校验失败的请求,监控异常的权限访问尝试,及时发现潜在的攻击行为。
内容的提问来源于stack exchange,提问作者Fabio Moggi
相关产品推荐
相关产品推荐

