262 Commits
Author SHA1 Message Date
Nicola Murino bca13cd448 vfs: skip listed names that address no entry
Listings leave out the entries naming the listed directory or its
parent, and the names holding a backslash, which no request can
address

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-09-28 20:11:13 +02:00
Nicola Murino 8cc691cb83 post-login hook: serialize a copy of the user
Fixes #2298

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-09-24 15:15:43 +02:00
Nicola Murino f9857a181c dataprovider: refuse self-service writes made from a stale snapshot
A user or admin changing its own password, profile or two-factor
settings wrote the whole account back and could overwrite a concurrent
administrator change. Self-service updates are now conditional on the
stored updated_at matching the value read, and every update stores a
strictly increasing updated_at.

Thanks to Thomas Toth for the report.

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-09-07 08:47:18 +02:00
Nicola Murino 80383acf38 dataprovider: refuse the login if a group setting uses %role% without a role
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-09-06 17:30:33 +02:00
Nicola Murino 10743720f7 httpd: check the account state when a public share is served
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-09-06 17:11:13 +02:00
Nicola Murino 89bd30c308 dumpdata: return an error if there are too many ip list entries
before we silently skipped the entries

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-08-30 07:47:10 +02:00
Nicola Murino 2fdfb94f55 reject a different username from the pre-login hook and auth plugins
Answering with another account has never been a supported configuration for
these two integrations. Mapping several login names onto a single account is a
feature of the external authentication hook

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-08-27 11:06:49 +02:00
Nicola Murino 447c6bfefd normalize permission keys and file pattern paths like every other virtual path
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-08-26 23:30:40 +02:00
Nicola Murino 27d8298384 rename: check the entries of a renamed directory, or refuse the rename
A directory rename authorized the whole tree from a walk performed before the
move, so the file_patterns filters and the per-directory rename permissions
decided on the tree as the walk had read it rather than on the one the move
carried.

The walk no longer authorizes the move. A gate decides from the configuration
alone whether anything can tell the entries of the tree apart: a move that
changes which file_patterns filters govern the contents, or a per-directory
permission that restricts renaming defined at or below either tree. When
something can, the object stores check every entry at the moment they move it,
since they rename a directory by recursing into it, and the filesystems that
rename it in a single operation refuse it, because nothing enumerates what that
operation carries. The type of the source is read again immediately before the
move.

Granting rename_files and rename_dirs separately keeps the pre-move walk.

So renaming a folder into or out of a path where a different file_patterns
filter applies is now refused on the local filesystem, SFTP and HTTP, and
checked entry by entry on the object stores; the same holds for a per-directory
permission that restricts renaming inside either tree. Renames the walk used to
refuse fail with the same error as before. IsRenameAtomic is derived from the
same capability instead of matching the filesystem name.

Reported-by: Wade Sparks (@wsparks-vc)

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-08-26 23:21:19 +02:00
Nicola Murino dc06275e09 shares: fix a directory share widened to the whole user filesystem
Downloading a public share limited to a single directory as a zip archive
rewrote share.Paths[0] to "/" so the entry names came out relative to that
directory. The value it overwrote is the policy every later public request is
authorized against: the browsable listing resolves the requested path under
Paths[0] and the upload handler derives its destination prefix from it.

The rewrite stays confined to the copy the handler holds only if the data
provider returns a share that owns its paths. The memory provider did not:
getACopy assigned the Paths slice by header, so the copy kept pointing at the
array stored in the provider map and the rewrite reached the stored share.
From that moment the link served the whole filesystem of the share owner. Any
recipient, with no account and nothing beyond the share URL, could list and
download every file the owner can reach, and a read-write share also accepted
uploads at the owner's root. The archive returned to the recipient who
triggered the change was still limited to the shared directory, so the
response revealed nothing. The SQL and bolt providers rebuild the slice while
decoding the stored row and were not affected.

The list of files to archive is now local to the handler, so the share is no
longer written to, and getACopy clones the paths, so a copy never shares state
with the provider.

The regression test downloads a share limited to one directory twice and
checks the archive, the stored paths, the browsable listing and a file outside
the share in between, with whichever data provider the test suite runs with.

Thanks to Yutaka Sasaki (@SAYUTIM) for the report.

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-08-25 09:32:54 +02:00
Nicola Murino b4698f1f4d dataprovider: refuse to delete referenced folders, roles and groups
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-08-24 18:27:56 +02:00
Nicola Murino d54965865e replace golangci-lint with staticcheck, errcheck and unparam
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-08-24 15:07:16 +02:00
Nicola Murino c7f96eb74e dataprovider: schema version 36
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-08-24 12:24:27 +02:00
Nicola Murino d0b755d1d1 dataprovider: refresh the cached admin state on shared databases
An instance that starts with an empty admins table caches that state until
restart, so it keeps serving the setup page after another instance completes
the initial configuration. Instances backed by a shared SQL provider now
re-check the database, rate limited to one query per second and only until
the first admin is found

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-08-24 11:23:54 +02:00
Nicola Murino de6bc600a3 common: report a hidden path as missing consistently
this is a best effort and outside the security boundaries

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-08-12 22:10:30 +02:00
Nicola Murino b7a2404f77 common: match the entries when a rename changes the filter scope
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-08-12 22:04:19 +02:00
Nicola Murino db5cda1388 webdavd: key the user cache on the stored username
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-08-12 21:17:07 +02:00
Nicola Murino 0da768877c dataprovider: apply the anonymous restrictions at runtime
they were applied during the validation

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-08-12 21:13:05 +02:00
Nicola Murino 641f263317 dataprovider: apply the naming rules to the username the external auth returns
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-08-12 21:05:43 +02:00
Nicola Murino 961999f54b dataprovider: pass the plugin auth scope down the keyboard interactive flow
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-08-12 21:01:52 +02:00
Nicola Murino 8f079955bb ftpd: refactor TLS requirement
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-08-12 20:59:48 +02:00
Nicola Murino c4092cee08 sftpd: ask for the password before resolving the account
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-08-12 20:53:46 +02:00
Nicola Murino 270f3fc431 httpd: tie share login tokens to the share signature
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-08-04 09:35:00 +02:00
Nicola Murino 6cce58233c virtual folders: add subpath support
A virtual folder mapping can now re-root the mount onto a sub-path of
the folder: the mount at the virtual path serves the folder starting
from the configured subpath, which stays invisible in the virtual tree.
The same folder can be mapped multiple times with distinct subpaths;
quota remains a property of the whole folder, so every mapping must use
the same limits.

Supported for local, encrypted, S3, GCS, Azure Blob and SFTP folders.

Fixes #2037

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-07-18 16:19:43 +02:00
Nicola Murino bafb7e7d6a modernize the codebase to current Go idioms
Apply the fixes suggested by the modernize analyzers enabled by
default in gopls v0.23: range over int, errors.AsType, min/max,
slices.Backward/Contains, maps.Copy, strings.Cut/CutPrefix/CutSuffix/
SplitSeq, fmt.Appendf, WaitGroup.Go and b.Loop in tests, new(expr)
instead of to.Ptr, removal of ineffective omitempty tags on nested
struct fields.

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-07-13 20:52:29 +02:00
Nicola Murino 0468e4b7d2 invalidate WebDAV user cache on user creation
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-07-09 19:42:52 +02:00
Nicola Murino 33b3aa0445 user: clear recovery codes in SetEmptySecrets
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-07-06 14:27:05 +02:00
Nicola Murino 661d77f47b vfs: migrate OsFs/CryptFs to os.Root and remove temp_path
Confine all local filesystem access to an os.Root, with path resoultion
enforced by the underlying OS.

Behavior changes an upgrading deployment may notice:

- temp_path is removed and ignored if still configured. Its purpose
  was to stage files (atomic uploads, scratch and streaming data)
  outside the user's storage and rename them into place, and that is
  fundamentally incompatible with the confinement: an os.Root cannot
  rename a file in from outside the root, so every staged upload would
  degrade to a full copy-then-delete even when temp_path is on the
  same filesystem, defeating the very atomicity the option existed to
  provide. It was not a great idea before this change either: a
  temp_path on a different device already turned the final rename into
  a silent full copy. Staged and scratch files (the atomic upload
  temporary, the CryptFs download pipe backing file) now live in the
  storage root itself, on the same filesystem as the data, so the
  final rename stays atomic.

- Cross-folder / cross-root renames are no longer atomic. A rename
  spanning the home and a local virtual folder, or two local folders,
  falls back to copy-then-delete: slower for large data even on the
  same device, and a midway failure can leave a partial tree at the
  destination. Symlinks and other non-regular entries inside a moved
  directory are skipped, not copied, and the rename still reports
  success. File mode bits are not preserved; regular files keep their
  modification time, directories do not.

- Absolute symlinks created outside SFTPGo, or by versions predating
  this change, are no longer traversable and Readlink rejects them with
  permission denied, even when the target is inside the root: os.Root
  refuses an absolute link target regardless of where it points.
  Symlinks created through SFTPGo are stored relative to the link
  directory and keep working.

- Execute-only home directories (e.g. mode 0111) are unusable:
  os.OpenRoot requires read permission, so every operation fails with
  permission denied until the directory is made readable, where
  previously only the listing failed. Since the root open is retried on
  demand, fixing the permissions (or creating a missing home
  externally) makes the same connection work again without
  reconnecting.

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-07-06 13:08:12 +02:00
Nicola Murino bf4b8f3446 remove some premature optimizations
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-06-08 12:56:38 +02:00
Nicola Murino 91d2893264 fix overquota transfers check
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-06-04 08:14:52 +02:00
Nicola Murino bec44b59fa keyboard interactive program: check for scanner errors
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-05-30 13:38:09 +02:00
Nicola Murino ac4fc3429d shares: enforce max_tokens atomically to close a usage-count race
Share usage was a check-then-write race: the public share handler
loaded a copy of the share, IsUsable() checked UsedTokens on that
in-memory snapshot, and UpdateShareLastUse() incremented the counter
afterwards with no lock, transaction or compare-and-swap spanning the
check. Concurrent requests for the same share could all pass the limit
check before any of them incremented the counter, so a share with a
usage cap (for example max_tokens=1) could be downloaded or uploaded
more times than allowed under concurrent access.

The usage count is now a conditional atomic reservation. For a positive
delta the SQL providers run a single guarded UPDATE that only succeeds
while used_tokens + n <= max_tokens and reject the request when no row
is affected; the memory and bolt providers apply the same guard inside
their existing locked section. UpdateShareLastUse returns
ErrShareUsageExceeded when the reservation would exceed the limit, and
the share download/upload handlers reject the request instead of
serving it. A non-positive delta is a refund and is always applied
unconditionally, so failed transfers still return their reserved token.

Thanks to Vishal Shukla (@shukla304) for the report.

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-05-18 18:50:53 +02:00
Nicola Murino 8fd48f5cfd fix lint warning
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-05-16 11:55:40 +02:00
Nicola Murino f950c3cf26 IP list: Fixed match when an IP is covered by multiple conflicting entries
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-05-15 11:17:47 +02:00
Nicola Murino 6e61c99b24 sqlite: improve connection string, use WAL mode
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-05-02 15:56:39 +02:00
Nicola Murino d2e15ea207 dataprovider: allow reading password from file
Also factor the file-or-value resolution in util.ResolveConfigValue
and reuse it for OIDC, KMS and httpd signing passphrase.

Fixes #2206

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-04-19 11:36:21 +02:00
Nicola Murino c9601a8976 fix new lint warnings
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-03-07 16:51:57 +01:00
Nicola Murino f1022db5c1 Apply naming rules when validating user-associated folders
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-01-17 14:14:51 +01:00
Nicola Murino e861f0f578 clean home dir after applying group replacement
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-01-16 19:49:06 +01:00
Nicola Murino f90fbccb2c ensure migration lock and transaction use the same connection
Previously, locks were session-bound but executed on a pool,
allowing race conditions.

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-01-12 18:44:22 +01:00
Nicola Murino 9db27aa782 remove restoreTemplateVars as it is no longer used
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-12-24 11:23:40 +01:00
Nicola Murino 3d549ce702 squash database migrations
also added shares_groups_mapping table, currently not used in the
open-source version

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-12-24 11:08:44 +01:00
Nicola Murino 0add546be3 user: fix group validation
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-12-17 20:01:16 +01:00
Nicola Murino ac3e59562d Enforce missing naming rule for actions and rules
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-12-07 10:35:13 +01:00
Nicola Murino 8c85a722a2 apply naming rules for related groups, roles and folders
also enforce stricter validation rules for usernames/names

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-12-06 22:08:22 +01:00
Nicola Murino 74f8539247 pre-login hook: require either a full user object or no user modification
The previous behavior was a leftover from an old refactor.
This change aligns the pre-login hook with the behavior of other hooks,
although it may break some edge cases that relied on the previous inconsistent
behavior.

Fixes #2107

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-11-18 20:09:22 +01:00
Nicola Murino 973b68a383 data provider: micro-optimization
skip availability check when updating the node timestamp.
If the update succeeds, the data provider is healthy

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-10-26 09:48:47 +01:00
Nicola Murino 5ce9688780 enforce group-level password strength for users and shares
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-10-26 09:44:32 +01:00
Nicola Murino 0ae2354fed JWT: replace jwtauth/jwx with lightweight wrapper around go-jose
We replaced the jwtauth and jwx libraries with a minimal custom wrapper
around go-jose because we don’t need the full feature set provided by jwx.
Implementing our own wrapper simplifies the codebase and improves
maintainability.

Moreover, go-jose depends only on the standard library, resulting in a
leaner dependency that still meets all our requirements.

This change also reduces the SFTPGo binary size by approximately 1MB

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-10-08 18:10:39 +02:00
Nicola Murino 35525e22e9 remove rsync support
rsync was executed as an external command, which means we have no insight
into or control over what it actually does.
From a security perspective, this is far from ideal.

To be clear, there's nothing inherently wrong with rsync itself. However,
if we were to support it properly within SFTPGo, we would need to implement
the low-level protocol internally rather than relying on launching an external
process. This would ensure it works seamlessly with any storage backend,
just as SFTP does, for example.
We recommend using one of the many alternatives that rely on the SFTP
protocol, such as rclone

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-09-28 18:15:15 +02:00