GitHub Advisoryadvisory · npm
GHSA-274f-6w77-8qm9 @sync-in/server vulnerable to TOTP Brute-Force via `POST /api/app/sync/register`
MEDIUM
Official page ↗
- Aliases
- CVE-2026-58271
- Ecosystem
- npm
- Packages
- @sync-in/server
- Severity
- Moderate
- CVSS vector
- CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:N
- Weaknesses
- CWE-307
- Reviewed
- 2026-09-22
Affected packages
• npm @sync-in/server: < 2.4.0 → fixed in 2.4.0
Description
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:
```typescript
@Post(SYNC_ROUTE.REGISTER)
@AuthTokenSkip()
register(@Body() syncClientRegistrationDto: SyncClientRegistrationDto, @Req() req: FastifyRequest): Promise {
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):
```typescript
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`:
```typescript
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
[W1] 195,400 total | 11.0 req/s | retries: 0
[*] Step 3: Results
Total attempts : 195,907
Time elapsed : 17769.5s (296.2min)
Average RPS : 11.0
[+] VALID TOTP CODE FOUND : 026961
[+] clientId : 13951b88-03d4-4854-8a29-cd8921d73d82
[+] clientToken : c92ef5ca-7432-44d0-8a94-56398bfe4117
[*] Step 4: Confirming access and attempting to disable 2FA...
[+] Authenticated as : mfatest (id=3, role=1)
passwordAttempts : 0
[+] 2FA DISABLED. Account 'mfatest' now accessible with password alone.
```
Measured observations:
• No account lockout was observed across 195,907 failed TOTP attempts in this test against the vulnerable version.
• The `clientToken` was exchanged for an authenticated session.
• MFA was disabled via `POST /api/auth/2fa/disable` while the guessed TOTP code was still valid and because the attacker already knew the account password.
Impact
An attacker who already knows valid credentials for a TOTP-enabled account with desktop sync permission can brute-force the second factor through `POST /api/app/sync/register` without triggering account lockout.
With `drift: 1`, 3 of 1,000,000 six-digit codes are valid per 30-second window (`p = 3/1,000,000`), giving an expected 333,333 attempts to find a valid code.
At 3 r/s, measured against a default single-worker deployment:
| Success probability | Attempts | Time at 3 r/s |
|---|---:|---:|
| 50% | 231,049 | 21.4 h |
| 90% | 767,528 | 71.1 h |
| 95% | 998,577 | 92.5 h |
| 99% | 1,535,056 | 142.1 h |
| Expected (mean) | 333,333 | 30.9 h |
Deployments with `server.workers > 1` may allow higher throughput, depending on CPU capacity and other bottlenecks. Throughput is heavily influenced by server-side password verification cost, worker count, database latency, and deployment limits, not only by the attacker's network speed.
Remediation
Add `&& success` to the freeze condition at `users-manager.service.ts` line 184:
```typescript
// Before
if (!isAuthTwoFa && configuration.auth.mfa.totp.enabled && user.twoFaEnabled) {
// After
if (!isAuthTwoFa && configuration.auth.mfa.totp.enabled && user.twoFaEnabled && success) {
```
The freeze still applies when a password succeeds but 2FA is pending, which was its intended purpose. A failed TOTP at `register()` and a failed password at `login`/`token` both fall through to the increment path, restoring lockout after 10 failures.
For defense-in-depth, apply an IP and/or account-based rate limiter to `POST /api/app/sync/register` and other pre-auth credential endpoints.
Related
Alias · CVE (1)
- CVE-2026-58271 Sync-in Server is an open-source platform for file storage, sharing, collaboration, and syncing. vulnerability
Weakness · CWE (1)
- CWE-307 Improper Restriction of Excessive Authentication Attempts weakness
References
- https://github.com/Sync-in/server/security/advisories/GHSA-274f-6w77-8qm9 web
- https://nvd.nist.gov/vuln/detail/CVE-2026-58271 advisory
- https://github.com/Sync-in/server/pull/228 web
- https://github.com/Sync-in/server/commit/b13a4aad5c2b38fe8231a0d007cd08a086ec5bdb web
- https://github.com/Sync-in/server package
- https://github.com/Sync-in/server/releases/tag/v2.4.0 web