Skip to content

Merge upstream release v2.7.6 - #5

Merged
MartinEpfl merged 69 commits into
mainfrom
upstream-sync/v2.7.6
Sep 28, 2026
Merged

MartinEpfl merged 69 commits into
mainfrom
upstream-sync/v2.7.6

Conversation

@baysl

@baysl baysl commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator

Merges upstream release v2.7.6 (drakkan/sftpgo) into main.

Workflows upstream ships are deleted by the sync job; this fork runs only build-image.yaml and sync-upstream.yaml.

No conflicts. Review the upstream changes, then merge.
After merge, publish a new Release to build the image.

drakkan added 30 commits July 26, 2026 13:00
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Invalidation was keyed on the exact token text, so an equivalent
Base64URL spelling of the same signed token did not match the stored
entry and kept working after a logout. The store is now keyed on the
"jti" claim, which is invariant across equivalent encodings of one token.

This is a correctness fix and not a privilege boundary: producing an
equivalent spelling requires already holding a valid signed token, it
grants no additional access, and the token remains usable only until its
own expiry, a bound it never exceeds.

Reported-by: Corban Villa (@corbanvilla)
Reported-by: Sohee Kim (@soh3e)
Reported-by: Austin Chu (@dderpym)

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
The token manager Add now reports whether the entry was persisted and the
provider-backed manager logs the failure, so an invalidation that does not
happen leaves a trace instead of being dropped in silence.

The REST admin and user logout propagate the error and answer 500: the
response states that the token has been invalidated, so it must not be
sent while the token is still usable. invalidateToken also reports an
error for a request without a verified token, there is nothing to
invalidate in that case.

Invalidation that is a side effect of another operation stays best effort
and does not gate that operation's result: the password is already
changed, the request is already rejected, the pre-login token goes away
with the cookie. Those call sites rely on the token manager log.

The web logout paths keep clearing the login cookie and redirecting
instead of rendering an error page: the client is logged out locally
either way, and a failed store write only matters to someone who already
holds a copy of the token.

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
… no code

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
…h returns

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
they were applied during the validation

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
…d it

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Computing a digest reads the whole file, so the hash commands now require the
download permission on the parent directory, the same check a download
performs. They previously accepted the list permission: accounts granted list
without download now get a permission denied error.

Reported-by: @cyny666

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
The read goes through a regular download transfer, so it honors the data
transfer quota and the bandwidth limit, it counts towards the transfer limits,
it is listed among the active transfers and can be monitored and aborted, and
it triggers the pre-download and download events.

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
…mlink

The file pattern filter matches names, and the operations that read an item
evaluate it on the item itself. Three did not agree with that: a copy
evaluated it on the target only, a rename mapped every refusal to a
permission error and so discarded the deny policy configured on the filter,
and symlink creation honored the hide policy alone, letting the default one
through.

They now all evaluate the source and report the configured policy through
GetErrorForDeniedFile, as stat, download and delete already do, so the same
filter gives the same answer whichever operation reaches a name. The target
keeps reporting a permission error: it is a path being written, not an item
being read. isRenamePermitted returns an error and is renamed accordingly;
the recursive walk still reports a permission error, because the entries it
walks are the contents of the directory being renamed and a hidden one among
them must not make that directory look missing.

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
this is a best effort and outside the security boundaries

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
…e copy

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
…ownload

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
drakkan and others added 28 commits August 24, 2026 11:47
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
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>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
useless on rename, permissions are preserved

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
…tual path

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
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>
Fixes drakkan#2282

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
The wrapper also always closes the files it opens

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
HTTP builds a connection per request, so checkHTTPClientUser is where a
request is authorized, the way the credential check is for SFTP and FTP:
it evaluates the login conditions of the account together with the
denied protocols, the login method and the source address.

An account that is disabled, that reaches its expiration date or that
falls outside its access time restrictions is refused on the next
request. The check is redundant on the paths that already run it before
the call, and costs nothing: the account is in memory.

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
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>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
# Conflicts:
#	.github/workflows/codeql.yml
#	.github/workflows/development.yml
#	.github/workflows/release.yml
@baysl
baysl force-pushed the upstream-sync/v2.7.6 branch from 0753b56 to d2ddbca Compare September 28, 2026 06:33
@MartinEpfl
MartinEpfl merged commit d3deefa into main Sep 28, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants