{"record":{"id":"1e3536b537dd9613","repo":"signalapp/Signal-Server","slug":"must-not-use-authenticated-connection-for-subscrib","errorCode":null,"errorMessage":"must not use authenticated connection for subscriber operations","messagePattern":"must not use authenticated connection for subscriber operations","errorType":"exception","errorClass":"SubscriptionForbiddenException","httpStatus":403,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/storage/SubscriberCredentials.java","lineNumber":33,"sourceCode":"import javax.crypto.Mac;\nimport javax.crypto.spec.SecretKeySpec;\nimport org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;\nimport org.whispersystems.textsecuregcm.subscriptions.SubscriptionException;\nimport org.whispersystems.textsecuregcm.subscriptions.SubscriptionForbiddenException;\nimport org.whispersystems.textsecuregcm.subscriptions.SubscriptionNotFoundException;\n\npublic record SubscriberCredentials(@Nonnull byte[] subscriberBytes,\n                             @Nonnull byte[] subscriberUser,\n                             @Nonnull byte[] subscriberKey,\n                             @Nonnull byte[] hmac,\n                             @Nonnull Instant now) {\n\n  public static SubscriberCredentials process(\n      final Optional<AuthenticatedDevice> authenticatedAccount,\n      final String subscriberId,\n      final Clock clock) throws SubscriptionException {\n    if (authenticatedAccount.isPresent()) {\n      throw new SubscriptionForbiddenException(\"must not use authenticated connection for subscriber operations\");\n    }\n    final byte[] subscriberBytes = convertSubscriberIdStringToBytes(subscriberId);\n    return process(subscriberBytes, clock);\n  }\n\n  public static SubscriberCredentials process(\n      final byte[] subscriberBytes,\n      final Clock clock) {\n    final Instant now = clock.instant();\n    final byte[] subscriberUser = getUser(subscriberBytes);\n    final byte[] subscriberKey = getKey(subscriberBytes);\n    final byte[] hmac = computeHmac(subscriberUser, subscriberKey);\n    return new SubscriberCredentials(subscriberBytes, subscriberUser, subscriberKey, hmac, now);\n  }\n\n  private static byte[] convertSubscriberIdStringToBytes(String subscriberId) throws SubscriptionNotFoundException {\n    try {\n      byte[] bytes = Base64.getUrlDecoder().decode(subscriberId);","sourceCodeStart":15,"sourceCodeEnd":51,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/storage/SubscriberCredentials.java#L15-L51","documentation":"SubscriberCredentials.process throws SubscriptionForbiddenException when a subscriber operation arrives over a connection already authenticated as a Signal account/device. Subscriber (donation/payment) operations use an anonymous, subscriberId-based credential model; mixing them with an authenticated account identity would let a logged-in user probe or hijack subscriber records, so the library deliberately forbids it.","triggerScenarios":"Calling SubscriberCredentials.process(Optional.of(authenticatedDevice), subscriberId, clock) — i.e. any resource method decorated with @Auth AuthenticatedDevice whose handler also resolves SubscriberCredentials from a subscriberId path/query parameter.","commonSituations":"A developer adds an authenticated account parameter to a subscription/donation endpoint for convenience and forgets the subscriber endpoints must be unauthenticated; copying an existing account-scoped resource class to build a subscriber-scoped one and keeping the @Auth annotation.","solutions":["Remove the AuthenticatedDevice parameter (and @Auth annotation) from the subscriber resource method so the connection is unauthenticated.","If account identity is genuinely needed, split the endpoint: account-authenticated routes must not call SubscriberCredentials.process; use the account-based storage APIs instead.","In tests/integration clients, stop sending Authorization/credential headers on subscriber-operation requests."],"exampleFix":"// before\npublic void donate(@Auth Optional<AuthenticatedDevice> auth, @PathParam(\"subscriberId\") String subscriberId) {\n  SubscriberCredentials creds = SubscriberCredentials.process(auth, subscriberId, clock);\n}\n// after\npublic void donate(@PathParam(\"subscriberId\") String subscriberId) {\n  SubscriberCredentials creds = SubscriberCredentials.process(Optional.empty(), subscriberId, clock);\n}","handlingStrategy":"validation","validationCode":"if (authenticatedAccount.isPresent()) {\n  throw new IllegalStateException(\"subscriber operations must not be performed on an authenticated connection\");\n}","typeGuard":"boolean isUnauthenticatedConnection(Optional<AuthenticatedDevice> auth) { return auth.isEmpty(); }","tryCatchPattern":"try {\n  SubscriberCredentials.process(auth, subscriberId, clock);\n} catch (SubscriptionForbiddenException e) {\n  log.warn(\"authenticated connection used for subscriber op\");\n  return Response.status(403).build();\n}","preventionTips":["Keep subscriber/donation resource methods free of @Auth AuthenticatedDevice parameters","Document the anonymous-credential contract on subscriber endpoints","Add an integration test asserting subscriber endpoints reject Authorization headers"],"tags":["auth","subscriptions","forbidden"],"backgroundTag":"permission-denied","analyzedSha":"100ab61c82627582c867d19e1c0561ba2781e927","analyzedAt":"2026-09-09T13:29:47.883Z","contentChangedAt":"2026-09-09T13:29:47.883Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}