Graylog vulnerable to privilege escalation through API tokens
Description
Graylog is a free and open log management platform. In versions 6.2.0 to before 6.2.4 and 6.3.0-alpha.1 to before 6.3.0-rc.2, Graylog users can gain elevated privileges by creating and using API tokens for the local Administrator or any other user for whom the malicious user knows the ID. For the attack to succeed, the attacker needs a user account in Graylog. They can then proceed to issue hand-crafted requests to the Graylog REST API and exploit a weak permission check for token creation. This issue has been patched in versions 6.2.4 and 6.3.0-rc.2. A workaround involves disabling the respective configuration found in System > Configuration > Users > "Allow users to create personal access tokens".
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.graylog2:graylog2-serverMaven | >= 6.2.0, < 6.2.4 | 6.2.4 |
org.graylog2:graylog2-serverMaven | >= 6.3.0-alpha.1, < 6.3.0-rc.2 | 6.3.0-rc.2 |
Affected products
1- Range: >= 6.2.0, < 6.2.4
Patches
29215b8f1fd32Merge commit from fork
3 files changed · +17 −24
changelog/unreleased/ghsa-3m86-c9x3-vwm9.toml+2 −0 added@@ -0,0 +1,2 @@ +type = "security" +message = "Fix permission check for creating a new API-token. See [GHSA-3m86-c9x3-vwm9](https://github.com/Graylog2/graylog2-server/security/advisories/GHSA-3m86-c9x3-vwm9) for details."
graylog2-server/src/main/java/org/graylog2/rest/resources/users/UsersResource.java+2 −6 modified@@ -727,13 +727,9 @@ public Token generateNewToken( @ApiParam(name = "name", value = "Descriptive name for this token (e.g. 'cronjob') ", required = true) @PathParam("name") String name, @ApiParam(name = "JSON Body", value = "Can optionally contain the token's TTL.", defaultValue = "{\"token_ttl\":null}") GenerateTokenTTL body) { final User futureOwner = loadUserById(userId); - final User currentUser = getCurrentUser(); - if (currentUser == null) { - throw new ForbiddenException("Not allowed to create tokens for unknown user."); - } - if (!isPermitted(USERS_TOKENCREATE, currentUser.getName())) { - throw new ForbiddenException(currentUser.getName() + " is not allowed to create token."); + if (!isPermitted(USERS_TOKENCREATE, futureOwner.getName())) { + throw new ForbiddenException("You are not allowed to create a token for user " + futureOwner.getName() + "."); } if (body == null) {
graylog2-server/src/test/java/org/graylog2/rest/resources/users/UsersResourceTest.java+13 −18 modified@@ -93,7 +93,6 @@ public class UsersResourceTest { private static final String TOKEN_NAME = "tokenName"; private static final String ADMIN_OBJECT_ID = new ObjectId().toHexString(); - private static final PeriodDuration PD_30_DAYS = PeriodDuration.parse("P30D"); @Rule public MockitoRule rule = MockitoJUnit.rule(); @@ -212,7 +211,6 @@ public void createTokenFailsIfCreateNotAllowed() { final UsersResource.GenerateTokenTTL ttl = new UsersResource.GenerateTokenTTL(Optional.of(PeriodDuration.of(Duration.ofDays(30)))); assertThrows(ForbiddenException.class, () -> usersResource.generateNewToken(USERNAME, TOKEN_NAME, ttl)); } finally { - verify(subject).getPrincipal(); verify(subject).isPermitted(USERS_TOKENCREATE + ":" + USERNAME); verifyNoMoreInteractions(clusterConfigService, accessTokenService); } @@ -227,7 +225,6 @@ public void createTokenSucceedsEvenWithNULLBody() { final Token actual = usersResource.generateNewToken(USERNAME, TOKEN_NAME, null); assertEquals(expected, actual); } finally { - verify(subject).getPrincipal(); verify(subject).isPermitted(USERS_TOKENCREATE + ":" + USERNAME); verify(clusterConfigService).getOrDefault(UserConfiguration.class, UserConfiguration.DEFAULT_VALUES); verify(accessTokenService).create(USERNAME, TOKEN_NAME, PeriodDuration.of(Duration.ofDays(30))); @@ -246,8 +243,7 @@ public void adminCanCreateTokensForOtherUsers() { final Token actual = usersResource.generateNewToken(USERNAME, TOKEN_NAME, null); assertEquals(expected, actual); } finally { - verify(subject).getPrincipal(); - verify(subject).isPermitted(USERS_TOKENCREATE + ":" + adminUserName); + verify(subject).isPermitted(USERS_TOKENCREATE + ":" + USERNAME); verify(clusterConfigService).getOrDefault(UserConfiguration.class, UserConfiguration.DEFAULT_VALUES); verify(accessTokenService).create(USERNAME, TOKEN_NAME, PeriodDuration.of(Duration.ofDays(30))); verifyNoMoreInteractions(clusterConfigService, accessTokenService); @@ -264,8 +260,7 @@ public void regularUserCannotCreateTokensForOtherUsers() { try { assertThrows(ForbiddenException.class, () -> usersResource.generateNewToken(USERNAME, TOKEN_NAME, null)); } finally { - verify(subject).getPrincipal(); - verify(subject).isPermitted(USERS_TOKENCREATE + ":" + otherUserName); + verify(subject).isPermitted(USERS_TOKENCREATE + ":" + USERNAME); verifyNoMoreInteractions(clusterConfigService, accessTokenService); } } @@ -290,14 +285,14 @@ private Token createTokenAndPrepareMocks(Map<String, Object> owningUser, Map<Str user.setRoleIds(Set.of(ADMIN_OBJECT_ID)); } - when(subject.getPrincipal()).thenReturn(callingUserName); - when(userService.loadById(callingUserName)).thenReturn(user); - when(roleService.getAdminRoleObjectId()).thenReturn(ADMIN_OBJECT_ID); + final boolean allowedToCreateToken = callingUserName.equals(owningUserName) || isAdmin; when(userManagementService.loadById(USERNAME)).thenReturn(userImplFactory.create(owningUser)); - when(subject.isPermitted(USERS_TOKENCREATE + ":" + callingUserName)).thenReturn(callingUserName.equals(owningUserName) || isAdmin); - when(clusterConfigService.getOrDefault(UserConfiguration.class, UserConfiguration.DEFAULT_VALUES)) - .thenReturn(UserConfiguration.create(false, Duration.of(8, ChronoUnit.HOURS), false, false, PeriodDuration.of(Duration.ofDays(30)))); - when(accessTokenService.create(USERNAME, UsersResourceTest.TOKEN_NAME, PeriodDuration.of(Duration.ofDays(30)))).thenReturn(accessToken); + when(subject.isPermitted(USERS_TOKENCREATE + ":" + owningUserName)).thenReturn(allowedToCreateToken); + if (allowedToCreateToken) { + when(clusterConfigService.getOrDefault(UserConfiguration.class, UserConfiguration.DEFAULT_VALUES)) + .thenReturn(UserConfiguration.create(false, Duration.of(8, ChronoUnit.HOURS), false, false, PeriodDuration.of(Duration.ofDays(30)))); + when(accessTokenService.create(USERNAME, UsersResourceTest.TOKEN_NAME, PeriodDuration.of(Duration.ofDays(30)))).thenReturn(accessToken); + } return Token.create(tokenId.toHexString(), TOKEN_NAME, token, lastAccess); @@ -308,7 +303,7 @@ private Token createTokenAndPrepareMocks(Map<String, Object> userProps, boolean final DateTime lastAccess = Tools.nowUTC(); final Map<String, Object> tokenProps = Map.of(AccessTokenImpl.NAME, TOKEN_NAME, AccessTokenImpl.TOKEN, token, AccessTokenImpl.LAST_ACCESS, lastAccess); final ObjectId tokenId = new ObjectId(); - final AccessToken accessToken = new AccessTokenImpl(tokenId, tokenProps); + final AccessToken accessToken = allowCreateToken ? new AccessTokenImpl(tokenId, tokenProps) : null; prepareMocks(userProps, accessToken, allowCreateToken); @@ -317,11 +312,11 @@ private Token createTokenAndPrepareMocks(Map<String, Object> userProps, boolean private void prepareMocks(Map<String, Object> userProps, AccessToken accessToken, boolean allow) { final User user = userImplFactory.create(userProps); - when(subject.getPrincipal()).thenReturn(USERNAME); - when(userService.loadById(USERNAME)).thenReturn(user); when(userManagementService.loadById(USERNAME)).thenReturn(user); when(subject.isPermitted(USERS_TOKENCREATE + ":" + USERNAME)).thenReturn(allow); - when(clusterConfigService.getOrDefault(UserConfiguration.class, UserConfiguration.DEFAULT_VALUES)).thenReturn(UserConfiguration.DEFAULT_VALUES); + if (allow) { + when(clusterConfigService.getOrDefault(UserConfiguration.class, UserConfiguration.DEFAULT_VALUES)).thenReturn(UserConfiguration.DEFAULT_VALUES); + } if (accessToken != null) { when(accessTokenService.create(USERNAME, UsersResourceTest.TOKEN_NAME, PeriodDuration.of(Duration.ofDays(30)))).thenReturn(accessToken); }
6936bd16a783Merge commit from fork
3 files changed · +17 −24
changelog/unreleased/ghsa-3m86-c9x3-vwm9.toml+2 −0 added@@ -0,0 +1,2 @@ +type = "security" +message = "Fix permission check for creating a new API-token. See [GHSA-3m86-c9x3-vwm9](https://github.com/Graylog2/graylog2-server/security/advisories/GHSA-3m86-c9x3-vwm9) for details."
graylog2-server/src/main/java/org/graylog2/rest/resources/users/UsersResource.java+2 −6 modified@@ -726,13 +726,9 @@ public Token generateNewToken( @ApiParam(name = "name", value = "Descriptive name for this token (e.g. 'cronjob') ", required = true) @PathParam("name") String name, @ApiParam(name = "JSON Body", value = "Can optionally contain the token's TTL.", defaultValue = "{\"token_ttl\":null}") GenerateTokenTTL body) { final User futureOwner = loadUserById(userId); - final User currentUser = getCurrentUser(); - if (currentUser == null) { - throw new ForbiddenException("Not allowed to create tokens for unknown user."); - } - if (!isPermitted(USERS_TOKENCREATE, currentUser.getName())) { - throw new ForbiddenException(currentUser.getName() + " is not allowed to create token."); + if (!isPermitted(USERS_TOKENCREATE, futureOwner.getName())) { + throw new ForbiddenException("You are not allowed to create a token for user " + futureOwner.getName() + "."); } if (body == null) {
graylog2-server/src/test/java/org/graylog2/rest/resources/users/UsersResourceTest.java+13 −18 modified@@ -88,7 +88,6 @@ public class UsersResourceTest { private static final String TOKEN_NAME = "tokenName"; private static final String ADMIN_OBJECT_ID = new ObjectId().toHexString(); - private static final PeriodDuration PD_30_DAYS = PeriodDuration.parse("P30D"); @Rule public MockitoRule rule = MockitoJUnit.rule(); @@ -196,7 +195,6 @@ public void createTokenFailsIfCreateNotAllowed() { final UsersResource.GenerateTokenTTL ttl = new UsersResource.GenerateTokenTTL(Optional.of(PeriodDuration.of(Duration.ofDays(30)))); assertThrows(ForbiddenException.class, () -> usersResource.generateNewToken(USERNAME, TOKEN_NAME, ttl)); } finally { - verify(subject).getPrincipal(); verify(subject).isPermitted(USERS_TOKENCREATE + ":" + USERNAME); verifyNoMoreInteractions(clusterConfigService, accessTokenService); } @@ -211,7 +209,6 @@ public void createTokenSucceedsEvenWithNULLBody() { final Token actual = usersResource.generateNewToken(USERNAME, TOKEN_NAME, null); assertEquals(expected, actual); } finally { - verify(subject).getPrincipal(); verify(subject).isPermitted(USERS_TOKENCREATE + ":" + USERNAME); verify(clusterConfigService).getOrDefault(UserConfiguration.class, UserConfiguration.DEFAULT_VALUES); verify(accessTokenService).create(USERNAME, TOKEN_NAME, PeriodDuration.of(Duration.ofDays(30))); @@ -230,8 +227,7 @@ public void adminCanCreateTokensForOtherUsers() { final Token actual = usersResource.generateNewToken(USERNAME, TOKEN_NAME, null); assertEquals(expected, actual); } finally { - verify(subject).getPrincipal(); - verify(subject).isPermitted(USERS_TOKENCREATE + ":" + adminUserName); + verify(subject).isPermitted(USERS_TOKENCREATE + ":" + USERNAME); verify(clusterConfigService).getOrDefault(UserConfiguration.class, UserConfiguration.DEFAULT_VALUES); verify(accessTokenService).create(USERNAME, TOKEN_NAME, PeriodDuration.of(Duration.ofDays(30))); verifyNoMoreInteractions(clusterConfigService, accessTokenService); @@ -248,8 +244,7 @@ public void regularUserCannotCreateTokensForOtherUsers() { try { assertThrows(ForbiddenException.class, () -> usersResource.generateNewToken(USERNAME, TOKEN_NAME, null)); } finally { - verify(subject).getPrincipal(); - verify(subject).isPermitted(USERS_TOKENCREATE + ":" + otherUserName); + verify(subject).isPermitted(USERS_TOKENCREATE + ":" + USERNAME); verifyNoMoreInteractions(clusterConfigService, accessTokenService); } } @@ -274,14 +269,14 @@ private Token createTokenAndPrepareMocks(Map<String, Object> owningUser, Map<Str user.setRoleIds(Set.of(ADMIN_OBJECT_ID)); } - when(subject.getPrincipal()).thenReturn(callingUserName); - when(userService.loadById(callingUserName)).thenReturn(user); - when(roleService.getAdminRoleObjectId()).thenReturn(ADMIN_OBJECT_ID); + final boolean allowedToCreateToken = callingUserName.equals(owningUserName) || isAdmin; when(userManagementService.loadById(USERNAME)).thenReturn(userImplFactory.create(owningUser)); - when(subject.isPermitted(USERS_TOKENCREATE + ":" + callingUserName)).thenReturn(callingUserName.equals(owningUserName) || isAdmin); - when(clusterConfigService.getOrDefault(UserConfiguration.class, UserConfiguration.DEFAULT_VALUES)) - .thenReturn(UserConfiguration.create(false, Duration.of(8, ChronoUnit.HOURS), false, false, PeriodDuration.of(Duration.ofDays(30)))); - when(accessTokenService.create(USERNAME, UsersResourceTest.TOKEN_NAME, PeriodDuration.of(Duration.ofDays(30)))).thenReturn(accessToken); + when(subject.isPermitted(USERS_TOKENCREATE + ":" + owningUserName)).thenReturn(allowedToCreateToken); + if (allowedToCreateToken) { + when(clusterConfigService.getOrDefault(UserConfiguration.class, UserConfiguration.DEFAULT_VALUES)) + .thenReturn(UserConfiguration.create(false, Duration.of(8, ChronoUnit.HOURS), false, false, PeriodDuration.of(Duration.ofDays(30)))); + when(accessTokenService.create(USERNAME, UsersResourceTest.TOKEN_NAME, PeriodDuration.of(Duration.ofDays(30)))).thenReturn(accessToken); + } return Token.create(tokenId.toHexString(), TOKEN_NAME, token, lastAccess); @@ -292,7 +287,7 @@ private Token createTokenAndPrepareMocks(Map<String, Object> userProps, boolean final DateTime lastAccess = Tools.nowUTC(); final Map<String, Object> tokenProps = Map.of(AccessTokenImpl.NAME, TOKEN_NAME, AccessTokenImpl.TOKEN, token, AccessTokenImpl.LAST_ACCESS, lastAccess); final ObjectId tokenId = new ObjectId(); - final AccessToken accessToken = new AccessTokenImpl(tokenId, tokenProps); + final AccessToken accessToken = allowCreateToken ? new AccessTokenImpl(tokenId, tokenProps) : null; prepareMocks(userProps, accessToken, allowCreateToken); @@ -301,11 +296,11 @@ private Token createTokenAndPrepareMocks(Map<String, Object> userProps, boolean private void prepareMocks(Map<String, Object> userProps, AccessToken accessToken, boolean allow) { final User user = userImplFactory.create(userProps); - when(subject.getPrincipal()).thenReturn(USERNAME); - when(userService.loadById(USERNAME)).thenReturn(user); when(userManagementService.loadById(USERNAME)).thenReturn(user); when(subject.isPermitted(USERS_TOKENCREATE + ":" + USERNAME)).thenReturn(allow); - when(clusterConfigService.getOrDefault(UserConfiguration.class, UserConfiguration.DEFAULT_VALUES)).thenReturn(UserConfiguration.DEFAULT_VALUES); + if (allow) { + when(clusterConfigService.getOrDefault(UserConfiguration.class, UserConfiguration.DEFAULT_VALUES)).thenReturn(UserConfiguration.DEFAULT_VALUES); + } if (accessToken != null) { when(accessTokenService.create(USERNAME, UsersResourceTest.TOKEN_NAME, PeriodDuration.of(Duration.ofDays(30)))).thenReturn(accessToken); }
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-3m86-c9x3-vwm9ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-53106ghsaADVISORY
- github.com/Graylog2/graylog2-server/commit/6936bd16a783c2944a3d2f1e83902062520f90e3ghsax_refsource_MISCWEB
- github.com/Graylog2/graylog2-server/commit/9215b8f1fd32566c31e6f7447ed864df3590c157ghsax_refsource_MISCWEB
- github.com/Graylog2/graylog2-server/security/advisories/GHSA-3m86-c9x3-vwm9ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.