projectsend.org

Changelog

ProjectSend 2.3.0

If you run ProjectSend on Apache or LiteSpeed, this is the release to take. It installed fine on both before. Then every download arrived empty and every thumbnail was broken. That is…

If you run ProjectSend on Apache or LiteSpeed, this is the release to take. It installed fine on both before. Then every download arrived empty and every thumbnail was broken. That is fixed, and you do not have to configure anything. Installations on nginx were never affected and nothing changes for them.

The rest is mostly security work. Most of it is the same kind of thing: a screen or an API endpoint that showed a little more than the person asking was allowed to see.

New

  • Downloads work on any web server. Your files sit outside the web root, so ProjectSend checks permission on every download before anything is sent. The fast way to finish is to hand the file to the web server. Each web server wants that asked for differently, and until now ProjectSend only knew how to ask nginx. On Apache and LiteSpeed it asked anyway, nothing answered, and the visitor got an empty file. Now it works out what it is talking to. If it cannot hand the file over, it sends the file itself, which is slower under load but works everywhere.
  • Apache and LiteSpeed can still have the fast version. Install mod_xsendfile (LiteSpeed needs no module), point XSendFilePath at your storage directory, and set PROJECTSEND_FILE_DELIVERY=xsendfile. See the upgrade notes.
  • The dashboard tells you which way downloads are going out. If PHP is sending them, there is a warning next to it and a short explanation of what that costs you and how to change it. This is the kind of thing that is invisible until the day the site falls over, so it says so up front.
  • Your logo and your watermark, on every installation. Upload a logo and it replaces ours in the sidebar and on your public pages. Add a watermark and it goes on the thumbnails and previews your clients and visitors see. Staff still see the originals, and the watermark is never written into the stored file, so you can turn it off again.
  • You can find out which build you are running. Two images can say "2.2.1" and contain different code. projectsend:status now reports the commit it was built from.
  • You will know if the nightly jobs stop running. When the scheduler dies, nothing looks wrong. You find out weeks later, when a file you expired is still downloadable. ProjectSend now reports when its scheduled work last ran and whether any of it failed.
  • You get told when the mailbox stops working, even when a send noticed the problem before the scheduled check did.

Closed holes in who can see what

  • #1745 — Gate the comment moderation surfaces on reading, not just on the library. Permission to moderate comments was letting somebody read them, which is not the same thing: on the moderation screen and through the API, a role that could moderate comments but could not open any file was shown every comment in the installation — the text, staff-only notes, the client each conversation belongs to, and a visitor's IP address — about files it would be refused on. Approving a comment over the API handed back its body the same way.

    Who this affected. Only installations with a custom role built that way. None of the roles ProjectSend ships is affected: Account Manager, the only one that moderates comments, can read files as well, and so can a System Administrator. If you did build such a role, it can no longer moderate — give it one of the file permissions (upload, edit files, or edit other people's files) and it works again, now seeing only the comments on files it can actually open.

  • #1759 — Publish the example Docker quickstart on the loopback address instead of every network interface. The example set TRUSTED_PROXIES: "*", which tells ProjectSend to believe the client address forwarded by whoever connects to it. That is right behind a reverse proxy and wrong when anyone can reach the container directly, because then anyone can claim any address: enough to walk past the login lockout, every rate limit, and the address written to the download log and to guest comments.

    Who this affected. Installations started from compose.example.yaml or from the Docker Hub page, where port 8080 was reachable from outside the machine. A published Docker port is not covered by a host firewall such as ufw, so this was often open without anyone intending it.

  • #1760 — Have the Docker image default to production. On first boot the image copied its settings from the development template, which sets APP_ENV=local and APP_DEBUG=true. Two things followed that you could not see from inside the application: every server error showed its stack trace — file, line and surrounding source — to whoever triggered it, signed in or not; and "reject known-breached passwords" never actually ran, while the security settings screen went on reporting it as switched on.

    Who this affected. Anyone who started the container without setting those two values: a plain docker run with a database address, the Portainer, unRAID and TrueNAS templates, or a Kubernetes manifest naming only the database and APP_URL. Installations using compose.example.yaml, which sets both correctly, were never affected.

  • The client portal dashboard lists only files that client can open. The API dashboard's recent activity is cut the same way.

  • Three lists were showing more than the viewer was allowed to see: the reassignment picker, the account conversion list, and the membership an API member write handed back.

  • Mail and storage credentials no longer end up in the boot configuration cache. A settings form that gets rejected no longer sends the credential back to the browser.

  • Connecting a sign-in provider asks for your password again. Every password prompt in front of an account now has its own rate limit instead of sharing one. A two-factor code is claimed in a single step, so the same code cannot be used twice.

  • An expired file no longer locks a whole group shut for staff assigned to particular clients. A shared folder's contents count towards what a client can reach. A client is added to the roster of the staff member who created them.

  • Whether something is an API request is decided by the route, not by a header the caller sets.

  • The interface font is served from your own installation. Loading a page no longer tells a font CDN who is reading it.

  • A stored filename can no longer push a control character into a response header.

Fixed

  • The zip progress bar stops polling when you leave the page.
  • A zip that fails to build no longer tells the person who asked for it why, in the server's words.
  • Previews are written to a temporary file first, so a half-written one is never served. A file's previews are deleted even when its storage cannot be reached.
  • An expiry date no longer moves because somebody else saved the file at the same time. Setting one through the API means what it means on the web form.
  • Updating a client through the API no longer wipes custom fields the request never mentioned.
  • The transfers chart lines up with the timezone its data is stored in.
  • Creating an account over a deleted one's email address is refused instead of crashing.
  • A comment still shows who wrote it after that account is deleted.
  • Marking a file as a new version no longer emails people about a file they already had.
  • The password reset and confirm-password screens say where the account's password actually lives, which matters if you use LDAP or a sign-in provider.
  • A refused upload names the quota you are actually up against. A bulk edit that is refused says which permission was missing.
  • Uploaded folders get the permissions the storage library actually asks for.
  • The public preview log no longer records the same view repeatedly.
  • Updating with update.sh no longer silently switches off route, event and view caching. The script wiped the compiled caches while replacing the files, which is also how ProjectSend recognised that you had cached them in the first place — so it rebuilt nothing, and every update quietly left the site slower than the install instructions promised.
  • Every new screen in this release is translated into all sixteen languages.

Before you upgrade, read the notes below.

Upgrade Notes

  • This upgrade adds two indexes to the activity log, and on a big installation that takes minutes. It is the slowest part. Nothing goes offline while it runs — the application keeps answering — but do not expect the migration to finish in seconds.

  • On Apache or LiteSpeed you need to do nothing, but there is something worth doing. Downloads will start working on their own. PHP will be sending them, which ties up a worker process for the whole of each download. That is fine on a quiet site and not fine on a busy one. To move to the fast path: install mod_xsendfile (LiteSpeed needs no module), allow your storage directory with XSendFilePath, then set PROJECTSEND_FILE_DELIVERY=xsendfile in .env. The dashboard will confirm the change.

  • If you copied the example Docker file, http://<your-server-ip>:8080 will stop answering. That is the change. Reach the application through your reverse proxy, as APP_URL describes. If your proxy runs on a different machine, publish the port on the interface it arrives from and replace TRUSTED_PROXIES: "*" with that address or subnet — the two settings only make sense together.

  • Docker: APP_ENV and APP_DEBUG set inside storage/.env no longer take effect. The image now sets them itself, and a real environment variable always beats that file. If you had turned debug on by editing storage/.env, pass -e APP_DEBUG=true (or environment: in compose) instead. Anything you already set that way keeps working unchanged.

Thanks to @denkfabrik-li, who wrote all forty-four pull requests in this release, and to @prbt2016, who reported the Apache download failure that started the delivery work.

Pull Requests Merged Since 2.2.1

The summary above is what changed. This is the paper trail, for anyone who wants to read the original change. No issues were closed in this cycle — the work arrived as pull requests.

  • #1718 — Narrow the reassignment picker to what a viewer may see
  • #1719 — Count a shared folder's contents as reach, not just the folder
  • #1720 — Stop an expired file locking a group shut for a scoped staff member
  • #1721 — Scope the API dashboard's recent actions to what the viewer may read
  • #1722 — Show the portal dashboard the files a client can actually open
  • #1723 — Stop a client PATCH clearing custom fields it never mentioned
  • #1725 — Write a rendition through a temporary file, and never serve an empty one
  • #1726 — Delete a file's renditions even when its own disk cannot be resolved
  • #1727 — Give an API expiry date the same meaning the web gives it
  • #1728 — Stop an expiry moving because somebody else saved the file
  • #1729 — Decide what is an API request from the route, not from the caller's headers
  • #1730 — Refuse to provision over a deleted account's address instead of crashing
  • #1731 — Fail a zip build without handing the requester the server's reason
  • #1732 — Debounce the public preview log the way the signed-in one already is
  • #1734 — Name the quota a client is actually held to when an upload is refused
  • #1735 — Stop an editable-once checkbox locking before anybody ticks it
  • #1736 — Put a client on the roster of the scoped staff member who created them
  • #1737 — Compare the transfers window against the column's own timezone
  • #1738 — Claim a TOTP code atomically instead of checking then writing
  • #1739 — Refresh a mailbox on the schedule under the lock a send would hold
  • #1740 — Leave the caches update.sh's own update command needs to see
  • #1741 — Ask about the zips queue on every path that could answer it
  • #1742 — Set the directory permission Flysystem actually reads
  • #1743 — Check the read half of the redirect rule at every door, not one
  • #1744 — Stop a version link telling people about a file they already had
  • #1745 — Gate the comment moderation surfaces on reading, not just on the library
  • #1746 — Say what expiry does to a client-scoped staff member's library
  • #1747 — Say which permission a bulk edit was actually missing
  • #1748 — Let a password reset know where the account's credentials live
  • #1749 — A deleted account is still the person who wrote the comment
  • #1750 — Tell the admins the mailbox is dead, even when a send noticed first
  • #1751 — Keep the mail and storage credentials out of the boot-config cache
  • #1752 — Bound the two preference endpoints by their own registries
  • #1753 — Narrow the conversion list to the clients its own refusal allows
  • #1754 — Narrow the membership an API member write hands back
  • #1755 — Give every password check in front of an account its own bucket
  • #1756 — Make linking a provider re-prove the password
  • #1757 — Stop a rejected settings form flashing the credential it carried
  • #1758 — Let the confirm-password screen ask where the password lives
  • #1759 — Publish the quickstart on loopback, since it trusts any proxy
  • #1760 — Have the production image default to production
  • #1761 — Serve the interface font from the installation, not from a font CDN
  • #1762 — Run the auth and settings screens through the translator
  • #1763 — Stop the zip poll when its page goes away
  • #1764 — Honour Laravel's placeholder case convention in t()