Skip to content

Commit aabd660

Browse files
committed
fix: confine app tokens to the projects and scopes they were granted
An app token carried the identity of the install's author — typically an organization owner — and only the project-scoped path capped it to the install's granted scopes. Everywhere else it acted with the author's own privileges, so an install could reach any endpoint its author could, including the one that repoints its own manifest and thereby widens its own scopes. App tokens are now denied by default and admitted only where a project context was actually established, which is the only place the scope cap applies. That replaces the URL regex the enablement check used to depend on: the regex matched `/v2/projects/` alone, so the same request against `/api/project/**` or `/api/repository/**` skipped the check and reached projects the app was never enabled for. Managing apps is refused outright. Those endpoints sit under the project an app is enabled for, so the project binding alone would admit them — an install whose grant covers project.edit could otherwise enable further apps, disable the ones already there, or mint a token belonging to another install. The scope cap also only existed in getCurrentPermittedScopes, so the checks performed inside handler bodies still fell back to the author's own permissions; they now apply the same cap. Alongside: a token inherits the read-only state of the session that minted it, an install whose author has been disabled no longer authenticates, an expired token is reported as expired rather than invalid, and a manifest may not claim a baseUrl on Tolgee's own origin — the iframe sandbox keeps `allow-scripts allow-same-origin`, which stops isolating anything if the app is served from the origin it is framed by.
1 parent 280ad5f commit aabd660

26 files changed

Lines changed: 778 additions & 142 deletions

File tree

backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/apps/AppSelfRegistrationController.kt

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ import io.tolgee.hateoas.organization.apps.AppInstallModel
1212
import io.tolgee.hateoas.organization.apps.AppInstallModelAssembler
1313
import io.tolgee.model.Organization
1414
import io.tolgee.model.UserAccount
15+
import io.tolgee.security.ratelimit.RateLimited
1516
import io.tolgee.service.apps.AppInstallService
1617
import io.tolgee.service.organization.OrganizationRoleService
1718
import io.tolgee.service.organization.OrganizationService
1819
import io.tolgee.service.security.UserAccountService
20+
import io.tolgee.util.constantTimeEquals
1921
import jakarta.validation.Valid
2022
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
2123
import org.springframework.web.bind.annotation.CrossOrigin
@@ -24,7 +26,6 @@ import org.springframework.web.bind.annotation.RequestBody
2426
import org.springframework.web.bind.annotation.RequestHeader
2527
import org.springframework.web.bind.annotation.RequestMapping
2628
import org.springframework.web.bind.annotation.RestController
27-
import java.security.MessageDigest
2829

2930
/**
3031
* Lets an app register itself against a running server, authenticating with the server-wide
@@ -49,6 +50,7 @@ class AppSelfRegistrationController(
4950
private val tolgeeProperties: TolgeeProperties,
5051
) {
5152
@PostMapping("/self-register")
53+
@RateLimited(5, isAuthentication = true)
5254
@Operation(
5355
summary = "Register an app using the server-wide registration secret",
5456
description =
@@ -104,13 +106,6 @@ class AppSelfRegistrationController(
104106
}
105107
}
106108

107-
private fun constantTimeEquals(
108-
a: String,
109-
b: String,
110-
): Boolean {
111-
return MessageDigest.isEqual(a.toByteArray(), b.toByteArray())
112-
}
113-
114109
companion object {
115110
const val REGISTRATION_SECRET_HEADER = "X-Tolgee-App-Registration-Secret"
116111
}

backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/apps/AppTokenEndpointController.kt

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import io.tolgee.exceptions.AuthenticationException
1010
import io.tolgee.exceptions.BadRequestException
1111
import io.tolgee.hateoas.apps.AppAccessTokenModel
1212
import io.tolgee.security.authentication.AppTokenService
13+
import io.tolgee.security.ratelimit.RateLimited
1314
import io.tolgee.service.apps.AppInstallService
15+
import io.tolgee.util.constantTimeEquals
1416
import jakarta.validation.Valid
1517
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
1618
import org.springframework.web.bind.annotation.CrossOrigin
@@ -39,6 +41,7 @@ class AppTokenEndpointController(
3941
private val tolgeeProperties: TolgeeProperties,
4042
) {
4143
@PostMapping("/token")
44+
@RateLimited(5, isAuthentication = true)
4245
@Operation(
4346
summary = "Exchange app client credentials for an access token",
4447
description =
@@ -70,18 +73,6 @@ class AppTokenEndpointController(
7073
)
7174
}
7275

73-
private fun constantTimeEquals(
74-
a: String,
75-
b: String,
76-
): Boolean {
77-
if (a.length != b.length) return false
78-
var result = 0
79-
for (i in a.indices) {
80-
result = result or (a[i].code xor b[i].code)
81-
}
82-
return result == 0
83-
}
84-
8576
companion object {
8677
private const val GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials"
8778
}

backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/project/ProjectAppsController.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import io.tolgee.model.enums.Scope
1111
import io.tolgee.security.ProjectHolder
1212
import io.tolgee.security.authentication.AppTokenService
1313
import io.tolgee.security.authentication.AuthenticationFacade
14+
import io.tolgee.security.authentication.DenyAppAccess
1415
import io.tolgee.security.authorization.RequiresProjectPermissions
1516
import io.tolgee.security.authorization.UseDefaultPermissions
1617
import io.tolgee.service.apps.AppEnablementService
@@ -30,6 +31,7 @@ import org.springframework.web.bind.annotation.RestController
3031
@ConditionalOnProperty(name = ["tolgee.apps.enabled"], havingValue = "true")
3132
@RequestMapping(value = ["/v2/projects/{projectId:[0-9]+}/apps"])
3233
@Tag(name = "Project Apps")
34+
@DenyAppAccess
3335
class ProjectAppsController(
3436
private val projectHolder: ProjectHolder,
3537
private val authenticationFacade: AuthenticationFacade,
@@ -124,6 +126,7 @@ class ProjectAppsController(
124126
installId = installId,
125127
userId = authenticationFacade.authenticatedUser.id,
126128
projectId = projectId,
129+
isReadOnly = authenticationFacade.isReadOnly,
127130
)
128131
return AppTokenModel(token = token)
129132
}

backend/app/src/main/kotlin/io/tolgee/configuration/WebSecurityConfig.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package io.tolgee.configuration
1919
import io.tolgee.component.ExceptionHandlerFilter
2020
import io.tolgee.component.TransferEncodingHeaderDebugFilter
2121
import io.tolgee.security.authentication.AdminAccessInterceptor
22+
import io.tolgee.security.authentication.AppAccessInterceptor
2223
import io.tolgee.security.authentication.AuthenticationFilter
2324
import io.tolgee.security.authentication.AuthenticationInterceptor
2425
import io.tolgee.security.authentication.EmailValidationInterceptor
@@ -75,6 +76,8 @@ class WebSecurityConfig(
7576
@Lazy
7677
private val projectAuthorizationInterceptor: ProjectAuthorizationInterceptor,
7778
@Lazy
79+
private val appAccessInterceptor: AppAccessInterceptor,
80+
@Lazy
7881
private val featureAuthorizationInterceptor: FeatureAuthorizationInterceptor,
7982
private val exceptionHandlerFilter: ExceptionHandlerFilter,
8083
) : WebMvcConfigurer {
@@ -144,6 +147,9 @@ class WebSecurityConfig(
144147
registry
145148
.addInterceptor(projectAuthorizationInterceptor)
146149
.addPathPatterns(*PROJECT_ENDPOINTS)
150+
// Must follow projectAuthorizationInterceptor: it decides based on whether a project context was
151+
// established for the request.
152+
registry.addInterceptor(appAccessInterceptor)
147153
registry
148154
.addInterceptor(adminAccessInterceptor)
149155
.addPathPatterns(*ADMIN_ENDPOINTS)

0 commit comments

Comments
 (0)