CVE-2026-58271
@sync-in/server vulnerable to TOTP Brute-Force via `POST /api/app/sync/register`
Sync-in Server is an open-source platform for file storage, sharing, collaboration, and syncing. Prior to version 2.4.0, POST /api/app/sync/register accepts credentials and a TOTP code to register a desktop sync client. On a failed TOTP, SyncClientsManager.register() calls updateAccesses(user, ip, false), which hits a freeze branch that writes passwordAttempts back unchanged. The counter never reaches USER_MAX_PASSWORD_ATTEMPTS (10) and the lockout gate never fires. A successful guess returns a {clientId, clientToken} pair. The token can be exchanged via POST /api/app/sync/auth/cookie for a full JWT. While the code is valid, the attacker can also call POST /api/auth/2fa/disable to permanently remove MFA. Version 2.4.0 patches the issue.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- High
- Privileges needed
- Low
- User interaction
- None
What an attacker can affect
- Confidentiality
- High
- Integrity
- High
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
6.8
@sync-in/server vulnerable to TOTP Brute-Force via `POST /api/app/sync/register`
Sync-in Server is an open-source platform for file storage, sharing, collaboration, and syncing. Prior to version 2.4.0, POST /api/app/sync/register accepts credentials and a TOTP code to register a desktop sync client. On a failed TOTP, SyncClientsManager.register() calls updateAccesses(user, ip, false), which hits a freeze branch that writes passwordAttempts back unchanged. The counter never reaches USER_MAX_PASSWORD_ATTEMPTS (10) and the lockout gate never fires. A successful guess returns a {clientId, clientToken} pair. The token can be exchanged via POST /api/app/sync/auth/cookie for a full JWT. While the code is valid, the attacker can also call POST /api/auth/2fa/disable to permanently remove MFA. Version 2.4.0 patches the issue.
- Record
CVE-2026-58271- CVSS 3.1
- 6.8 Medium
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:N - Weakness
- CWE-307
- Updated
- 2026-11-02
EUVDEUVD-2026-84112
6.8
Sync-in Server is an open-source platform for file storage, sharing, collaboration, and syncing. Prior to version 2.4.0, POST /api/app/sync/register accepts credentials and a TOTP code to register a desktop sync client. On a failed TOTP, SyncClientsManager.register() calls updateAccesses(user, ip, false), which hits a freeze branch that writes passwordAttempts back unchanged. The counter never reaches USER_MAX_PASSWORD_ATTEMPTS (10) and the lockout gate never fires. A successful guess returns a {clientId, clientToken} pair. The token can be exchanged via POST /api/app/sync/auth/cookie for a full JWT. While the code is valid, the attacker can also call POST /api/auth/2fa/disable to permanently remove MFA. Version 2.4.0 patches the issue.
- Record
EUVD-2026-84112- CVSS 3.1
- 6.8 Medium
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:N - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-274f-6w77-8qm9
6.8
@sync-in/server vulnerable to TOTP Brute-Force via `POST /api/app/sync/register`
Affected component: Sync-in Server v2.3.0, POST /api/app/sync/register.
Required attacker capability: Valid login and password for a TOTP-enabled account with desktop sync permission.
Summary
POST /api/app/sync/register accepts credentials and a TOTP code to register a desktop sync client. In the vulnerable version, on a failed TOTP attempt, SyncClientsManager.register() called updateAccesses(user, ip, false), which hit a freeze branch that wrote passwordAttempts back unchanged. The counter never reached USER_MAX_PASSWORD_ATTEMPTS (10), so the account lockout gate never fired for repeated TOTP failures through this endpoint.
A successful TOTP guess registers a sync client and returns a {clientId, clientToken} pair, provided the account has the required desktop app permission and the registration payload is valid. The token can then be exchanged via POST /api/app/sync/auth/cookie for an authenticated session. While the guessed TOTP code is still valid, and because the attacker already knows the password, the attacker can also call POST /api/auth/2fa/disable to remove MFA.
Details
The endpoint is declared at sync.controller.ts line 71. @AuthTokenSkip() bypasses the bearer-token guard, so the route is reachable without any prior session:
@Post(SYNC_ROUTE.REGISTER)
@AuthTokenSkip()
register(@Body() syncClientRegistrationDto: SyncClientRegistrationDto, @Req() req: FastifyRequest): Promise<SyncClientAuthRegistration> {
return this.syncClientsManager.register(syncClientRegistrationDto, req.ip)
}
Inside SyncClientsManager.register(), after both the TOTP code and the recovery code are rejected, the handler fires a fire-and-forget access update and throws (sync-clients-manager.service.ts line 73):
this.usersManager.updateAccesses(user, ip, false).catch((e: Error) => this.logger.error({ tag: this.register.name, msg: `${e}` }))
throw new HttpException(authCode.message, HttpStatus.UNAUTHORIZED)
In the vulnerable version, updateAccesses() at users-manager.service.ts line 182 defaulted isAuthTwoFa to false:
async updateAccesses(user: UserModel, ip: string, success: boolean, isAuthTwoFa = false) {
let passwordAttempts: number
if (!isAuthTwoFa && configuration.auth.mfa.totp.enabled && user.twoFaEnabled) {
passwordAttempts = user.passwordAttempts
} else {
passwordAttempts = success ? 0 : Math.min(user.passwordAttempts + 1, USER_MAX_PASSWORD_ATTEMPTS)
}
await this.usersQueries.updateUserOrGuest(user.id, {
...
passwordAttempts: passwordAttempts,
isActive: user.isActive && passwordAttempts < USER_MAX_PASSWORD_ATTEMPTS
})
}
When register() called updateAccesses(user, ip, false), isAuthTwoFa defaulted to false. The condition on line 184 evaluated to true when TOTP was enabled site-wide and the account had it active. The else branch with Math.min(user.passwordAttempts + 1, ...) was never reached. passwordAttempts was written back unchanged, and the lockout gate in validateUserAccess() at line 89 never fired for repeated TOTP failures through this endpoint.
The freeze was designed for the web login flow, where a correct password at POST /api/auth/login produces a partial session and the counter should be preserved until POST /api/auth/2fa/login/verify completes. That route calls authProvider2FA.verify(body, req, true), which passes isAuthTwoFa=true into updateAccesses() and correctly increments on 2FA failure. The register() endpoint reused updateAccesses() for an outright TOTP rejection while passing the default isAuthTwoFa=false, triggering the freeze incorrectly.
The same freeze also applied to logUser() (users-manager.service.ts line 69), called by both POST /api/auth/login and POST /api/auth/token. On a wrong password for a 2FA-enabled account, updateAccesses(user, ip, false) was called without an isAuthTwoFa argument, so the freeze fired and…
- Record
GHSA-274f-6w77-8qm9- CVSS 3.1
- 6.8 Medium
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:N - Weakness
- CWE-307
- Updated
- 2026-11-02
NVD
6.8
Sync-in Server is an open-source platform for file storage, sharing, collaboration, and syncing. Prior to version 2.4.0, POST /api/app/sync/register accepts credentials and a TOTP code to register a desktop sync client. On a failed TOTP, SyncClientsManager.register() calls updateAccesses(user, ip, false), which hits a freeze branch that writes passwordAttempts back unchanged. The counter never reaches USER_MAX_PASSWORD_ATTEMPTS (10) and the lockout gate never fires. A successful guess returns a {clientId, clientToken} pair. The token can be exchanged via POST /api/app/sync/auth/cookie for a full JWT. While the code is valid, the attacker can also call POST /api/auth/2fa/disable to permanently remove MFA. Version 2.4.0 patches the issue.
- Record
CVE-2026-58271- CVSS 3.1
- 6.8 Medium
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:N - Weakness
- CWE-307
- Updated
- 2026-11-02
OSV
6.8
@sync-in/server vulnerable to TOTP Brute-Force via `POST /api/app/sync/register`
Sync-in Server is an open-source platform for file storage, sharing, collaboration, and syncing. Prior to version 2.4.0, POST /api/app/sync/register accepts credentials and a TOTP code to register a desktop sync client. On a failed TOTP, SyncClientsManager.register() calls updateAccesses(user, ip, false), which hits a freeze branch that writes passwordAttempts back unchanged. The counter never reaches USER_MAX_PASSWORD_ATTEMPTS (10) and the lockout gate never fires. A successful guess returns a {clientId, clientToken} pair. The token can be exchanged via POST /api/app/sync/auth/cookie for a full JWT. While the code is valid, the attacker can also call POST /api/auth/2fa/disable to permanently remove MFA. Version 2.4.0 patches the issue.
- Record
CVE-2026-58271- CVSS 3.1
- 6.8 Medium
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:N - Weakness
- CWE-307
- Updated
- 2026-11-02
OSVGHSA-274f-6w77-8qm9
6.8
@sync-in/server vulnerable to TOTP Brute-Force via `POST /api/app/sync/register`
Affected component: Sync-in Server v2.3.0, POST /api/app/sync/register.
Required attacker capability: Valid login and password for a TOTP-enabled account with desktop sync permission.
Summary
POST /api/app/sync/register accepts credentials and a TOTP code to register a desktop sync client. In the vulnerable version, on a failed TOTP attempt, SyncClientsManager.register() called updateAccesses(user, ip, false), which hit a freeze branch that wrote passwordAttempts back unchanged. The counter never reached USER_MAX_PASSWORD_ATTEMPTS (10), so the account lockout gate never fired for repeated TOTP failures through this endpoint.
A successful TOTP guess registers a sync client and returns a {clientId, clientToken} pair, provided the account has the required desktop app permission and the registration payload is valid. The token can then be exchanged via POST /api/app/sync/auth/cookie for an authenticated session. While the guessed TOTP code is still valid, and because the attacker already knows the password, the attacker can also call POST /api/auth/2fa/disable to remove MFA.
Details
The endpoint is declared at sync.controller.ts line 71. @AuthTokenSkip() bypasses the bearer-token guard, so the route is reachable without any prior session:
@Post(SYNC_ROUTE.REGISTER)
@AuthTokenSkip()
register(@Body() syncClientRegistrationDto: SyncClientRegistrationDto, @Req() req: FastifyRequest): Promise<SyncClientAuthRegistration> {
return this.syncClientsManager.register(syncClientRegistrationDto, req.ip)
}
Inside SyncClientsManager.register(), after both the TOTP code and the recovery code are rejected, the handler fires a fire-and-forget access update and throws (sync-clients-manager.service.ts line 73):
this.usersManager.updateAccesses(user, ip, false).catch((e: Error) => this.logger.error({ tag: this.register.name, msg: `${e}` }))
throw new HttpException(authCode.message, HttpStatus.UNAUTHORIZED)
In the vulnerable version, updateAccesses() at users-manager.service.ts line 182 defaulted isAuthTwoFa to false:
async updateAccesses(user: UserModel, ip: string, success: boolean, isAuthTwoFa = false) {
let passwordAttempts: number
if (!isAuthTwoFa && configuration.auth.mfa.totp.enabled && user.twoFaEnabled) {
passwordAttempts = user.passwordAttempts
} else {
passwordAttempts = success ? 0 : Math.min(user.passwordAttempts + 1, USER_MAX_PASSWORD_ATTEMPTS)
}
await this.usersQueries.updateUserOrGuest(user.id, {
...
passwordAttempts: passwordAttempts,
isActive: user.isActive && passwordAttempts < USER_MAX_PASSWORD_ATTEMPTS
})
}
When register() called updateAccesses(user, ip, false), isAuthTwoFa defaulted to false. The condition on line 184 evaluated to true when TOTP was enabled site-wide and the account had it active. The else branch with Math.min(user.passwordAttempts + 1, ...) was never reached. passwordAttempts was written back unchanged, and the lockout gate in validateUserAccess() at line 89 never fired for repeated TOTP failures through this endpoint.
The freeze was designed for the web login flow, where a correct password at POST /api/auth/login produces a partial session and the counter should be preserved until POST /api/auth/2fa/login/verify completes. That route calls authProvider2FA.verify(body, req, true), which passes isAuthTwoFa=true into updateAccesses() and correctly increments on 2FA failure. The register() endpoint reused updateAccesses() for an outright TOTP rejection while passing the default isAuthTwoFa=false, triggering the freeze incorrectly.
The same freeze also applied to logUser() (users-manager.service.ts line 69), called by both POST /api/auth/login and POST /api/auth/token. On a wrong password for a 2FA-enabled account, updateAccesses(user, ip, false) was called without an isAuthTwoFa argument, so the freeze fired and…
- Record
GHSA-274f-6w77-8qm9- CVSS 3.1
- 6.8 Medium
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:N - Weakness
- CWE-307
- Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
@sync-in/servernpm |
through 2.3.0 | 2.4.0 |
@sync-in/servernpm |
before 2.4.0 | 2.4.0 |
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-58271 is a medium vulnerability (CVSS 6.8) in Devolutions Server, published September 21, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.22%, the 12th percentile. Fixed in @sync-in/server 2.4.0.
References
github.com/Sync-in/server/security/advisories/GHSA-274f-6w77-8qm9
nvd.nist.gov/vuln/detail/CVE-2026-58271
github.com/Sync-in/server/pull/228
github.com/Sync-in/server/commit/b13a4aad5c2b38fe8231a0d007cd08a086ec5bdb
github.com/Sync-in/server/releases/tag/v2.4.0
github.com/advisories/GHSA-274f-6w77-8qm9
github.com/CVEProject/cvelistV5/tree/main/cves/2026/58xxx/CVE-2026-58271.json
github.com/Sync-in/server
Left out here: the rest of 2 source descriptions. The JSON record has them.