owncloud / owncloud/ocis

Public link brute-force protection blocks per token for all users, with no operator visibility

Open
#12,825 4 comments 0 reactions 0 assignees View on GitHub
Dominant language
Go
Stars
2.1k
Forks
274
Avg merge
2d 1h
Merged PRs (30d)
103

Description

### Describe the bug

The public-link brute-force protection blocks **per share token**, not per client. Any visitor who has the link can therefore lock it for everyone else by submitting wrong passwords a handful of times, and there is no way for an administrator to see that it happened or to lift the block.

Two separate problems, both rooted in the same design:

**1. The block is global for the link, and the counter has no client dimension**

In `owncloud/reva`, `pkg/auth/manager/publicshares/bruteforceprotection.go` stores attempts under the share token alone:

```go
func (bfp *BruteForceProtection) AddAttemptAndCheckAllow(ctx context.Context, shareToken string) (bool, error)
func (bfp *BruteForceProtection) Verify(ctx context.Context, shareToken string) bool
...
records, err := bfp.store.Read(shareToken)
```

`services/storage-publiclink/README.md` states this explicitly:

> If the public link is blocked by the brute force protection, it will be blocked for all the users.

With the defaults (`STORAGE_PUBLICLINK_BRUTEFORCE_MAXATTEMPTS=5`, `STORAGE_PUBLICLINK_BRUTEFORCE_TIMEGAP=1h`), five wrong passwords make a shared link unusable for every recipient for up to an hour. That is a denial-of-service primitive available to anyone holding the link, and it is also very easy to trigger by accident when a link is distributed to a group — a few people mistyping the password is enough.

**2. A block is invisible to operators**

The only trace of the decision is at debug level:

```go
sublog.Debug().
Int("attemptCount", attemptCount).
Bool("stillAccessible", attemptCount <= bfp.maxAttempts).
Msg("Failed attempt registered for brute force protection")
```

At the default `info` level nothing is logged when a link becomes blocked. There is also no admin API or CLI to list currently blocked tokens or to clear one, so the only remedies are to wait out `TIMEGAP` or to raise `MAXATTEMPTS` and restart.

In practice this meant we spent a long time on the wrong trail: from the user's side the link simply "stopped working" hours after creation, and the only way we found the cause was grepping the proxy access log for the token and noticing the `401 -> 429` transition.

### Steps to reproduce

```textarea
1. Create a password-protected public link and share it
2. From any browser, submit a wrong password 5 times within an hour
3. The link now returns 429 for every visitor, including the owner
4. Check the oCIS log at the default info level - nothing indicates the link is blocked
```

### Expected behavior

- Failed attempts are counted per client (e.g. IP, or an IP + token composite key) so that one visitor cannot lock out the rest.
- A block is logged at `info`/`warn` level, including the token and when it expires.
- Operators have some way to inspect and clear blocks (admin API, CLI, or metric).

### Actual behavior

Blocking is per token and global; nothing is logged at the default level; there is no way to inspect or clear it.

### Setup

```markdown
oCIS 8.0.4
STORAGE_PUBLICLINK_BRUTEFORCE_MAXATTEMPTS (unset -> default 5)
STORAGE_PUBLICLINK_BRUTEFORCE_TIMEGAP (unset -> default 1h)
OCIS_LOG_LEVEL=info
```

### Additional context

The user-facing half of this — web renders the resulting `429` as "Unknown error" — is filed separately.

#11983 also touches this code but is about TTL/cleanup of the stored attempt data, not about the blocking granularity or its observability.

I appreciate that per-IP counting weakens protection against a distributed attacker, so a composite key (or making the granularity configurable) may be the better trade-off than replacing the token key outright.

Contributor guide

Open the contributing guide

Research direction

Start with owncloud/reva/pkg/auth/manager/publicshares/bruteforceprotection.go and the behavior documented in services/storage-publiclink/README.md. Reproduce the five-failed-password flow with the listed environment defaults and inspect how attempts are stored and logged. Done should address client-aware blocking and give operators a visible way to identify and clear or manage blocked links.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
backend, observability, security
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.