Semaphore UI and HFS: two authorization flaws we reported
GHSA-ffp6-w57q-758q and GHSA-c3w7-ff56-j345: in Semaphore UI a Manager could move another project's vault into their own, in HFS an anonymous visitor could replace a file another visitor had just uploaded. What we found and which versions to install.

On 9 November 2026, within the space of an hour, two very different open source projects published a security advisory with the same name in the credits. The first is Semaphore UI, the web interface many teams use to run Ansible and Terraform against their servers. The second is HFS, the file server that since 2003 has let people share a folder on their own computer through a plain browser. In Semaphore, a user of one project could take the vault configuration of a template away from another project. In HFS, an anonymous visitor could replace the file another anonymous visitor had just uploaded, in a folder where deleting was forbidden.
These are advisories GHSA-ffp6-w57q-758q, fixed in Semaphore UI 2.19.20, and GHSA-c3w7-ff56-j345, fixed in HFS 3.4.1. We reported them: both findings are by Liam Giannini (Liam-G on GitHub), ethical hacker and co-founder of Webcracy. Our thanks to the Semaphore maintainers and to Massimo Melina, the author of HFS, who published the advisories and the fixed releases on the same day.
The two flaws look worlds apart, but they share a root. In both cases the program checked an identity; it just checked the wrong one.
Webcracy and cybersecurity
Besides building websites and applications, Webcracy works across the whole of cybersecurity: we protect networks and servers, watch over them with our SOC day and night, step in when there is an incident, do audits, consulting and system hardening, and put the defences to the test with penetration tests and code review. Research on the open source software that we and our clients use is one part of that work. Liam looks for flaws the way an attacker would, but to get them closed: he reports them privately to the maintainers and makes them public only once a fix is available. It is the same responsible disclosure we followed for ProjectSend, exarch and FreeScout.
Semaphore UI: the vault that moved from one project to another
Why Semaphore UI matters
Semaphore UI is a web interface and API for running Ansible, Terraform, OpenTofu, Terragrunt, Pulumi and PowerShell without going through the command line. It has existed since 2014, back when it was still called Ansible Semaphore; it is written in Go and released under the MIT licence. On GitHub it has more than 14,400 stars, over 1,300 forks and more than 180 contributors; the official Docker image semaphoreui/semaphore has passed 8.6 million pulls, plus about 600,000 for the old ansiblesemaphore/semaphore image. The project's website claims more than 100,000 installations: that figure comes from the vendor, but the order of magnitude fits everything else. Next to the open source version there are paid Pro and Enterprise editions.
In the Ansible world it is the lightweight alternative to AWX and to Red Hat's Ansible Automation Platform: a single binary that can start with a SQLite database and does not need a Kubernetes cluster the way AWX does. That makes it a common choice for anyone who wants a UI for Ansible without standing up yet another piece of infrastructure.
And here an authorization flaw weighs more than elsewhere. A tool like this holds the SSH keys, the cloud credentials, the passwords that unlock encrypted secrets: it is the point from which commands go out to the whole infrastructure. That is why it is split into projects, and often the same installation serves several teams, or several clients. The boundary between one project and another is, in practice, the boundary of who can touch what.
What template vaults are
In Semaphore a template describes a job to run: which playbook, against which inventory, with which variables. Many playbooks contain secrets encrypted with Ansible Vault, and decrypting them takes a password. The template says where to get it through one or more vault bindings: a key stored in the project, or a script that fetches the password from an external system. In the database each binding is a row in the project__template_vault table, with its own numeric ID, and the project and template it belongs to.
Where the check was missing
To create a template you call POST /api/project/{project_id}/templates. The middleware correctly checks that the user is a member of the project named in the URL and is allowed to manage its resources, which the Manager role permits. The new template is assigned to that project, but the vaults list reaches the database exactly as the client sent it.
The flaw was in the UpdateTemplateVaults function. For every entry with a non-zero ID, it assumed it was looking at an existing binding to update, and ran:
UPDATE project__template_vault
SET project_id=?, template_id=?, vault_key_id=?, name=?, type=?, script=?
WHERE id=?
The row was selected by id alone, with no check on which project it belonged to, and the update rewrote precisely project_id and template_id, handing them to the attacker's new template. The later deletion of leftover bindings was scoped by project, but it ran after the row had already been moved.
So all an attacker had to do was create a template in their own project and put the ID of one of the victim project's bindings into the vaults list. With the script type the key field is cleared, so nothing else about the other project needed to be known. And the IDs are numbers the database assigns in sequence, not secrets. The check looked at the project in the URL, but the object being modified was chosen by a number in the request body: the CWE catalogue calls this CWE-639, authorization bypass through a user-controlled key, and CWE-863, incorrect authorization.
How serious it is
After the request, the victim project's template no longer has its vault binding, and the attacker's template does. The most likely result is that runs needing to decrypt secrets fail until someone recreates the configuration. We did not measure that in our test: it is a plausible consequence, not a demonstrated one.
We are just as precise about what we did not find: no reading of secrets, no use of the victim's key, no code execution. The attacker also needs the Manager role, or equivalent permissions, in at least one project. That is why the severity is moderate. But in a tool that orchestrates infrastructure, "moderate" still means a user could modify a project where they had no role at all, and whoever runs the projects of other teams or clients would not have seen it coming.
How we verified it
The problem came to light by reading the code of Semaphore UI 2.19.18 (commit b621d14) and following the vaults list from the router to the SQL query. To confirm it, Liam wrote a regression test in Go that uses the release's in-memory SQLite store and its original components: project middleware, permission check, template creation and the SQL layer. The test creates two projects, A and B, makes the user a Manager in A only, creates a template with a vault in B, then asks to create a template in A carrying the ID of B's binding. The API answered 201, B's template went from one binding to zero, and A's new template contained B's original one.
The same query has been in Semaphore's code since at least version 2.11. The advisory lists only the version we verified, 2.19.18, and does not establish which earlier versions can be exploited the same way. It is safer to assume they can.
The fix in 2.19.20
Version 2.19.20 contains the fix, in commit 9445ee9 from pull request #4414. The update no longer touches project_id and template_id, and it only finds the row if it really belongs to the authorized project and template:
UPDATE project__template_vault
SET vault_key_id=?, name=?, type=?, script=?
WHERE id=? AND project_id=? AND template_id=?
An ID from another project no longer has any effect. The maintainers also added a test, TestManagerCannotMoveForeignProjectVault, which reproduces the advisory's scenario. In our report we had also suggested rejecting the request when the update matches no row, and creating the template and its bindings in a single transaction: the published fix closes the attack path, and the other measures remain the maintainers' choice.
What to do if you use Semaphore UI
Update to at least 2.19.20, or better to 2.19.21, released a few minutes later, which according to its release notes fixes another issue: a user could change their own password through the API without knowing the current one. With Docker, pin an explicit version instead of latest.
After updating, check that the templates using Ansible Vault still have their bindings: if one lost its vault for no obvious reason, you now know one possible cause. And review who holds the Manager role, and in which projects. If the separation between two groups of users is truly critical, say between different clients of a service provider, the projects of a single installation are a useful boundary but not an absolute one: separate instances, with separate databases, limit the damage of the next authorization bug.
HFS: the freshly uploaded file another anonymous visitor could replace
Why HFS matters
HFS, HTTP File Server, is one of those programs that anyone who has worked in IT for a few years has almost certainly come across. Massimo Melina, known online as rejetto, has been developing it since the early 2000s: you start it on your computer, choose the folders to share, and anyone with the address can download or upload files with a browser, with no cloud service in between. On SourceForge the project counts more than 970,000 downloads since 2003, not counting those from the author's website. HFS 3, rewritten in TypeScript on Node.js and released under the GPL-3.0, runs on Windows, Linux, macOS, FreeBSD and Android. On GitHub it has almost 4,000 stars and more than 370 forks; its release packages have been downloaded more than 500,000 times, the Docker image rejetto/hfs more than 116,000 times, and the npm package about 44,000 times in the past year.
It lends itself to moving files around a local network or receiving documents from clients and collaborators: settings where a folder open to anonymous uploads but protected from deletion is a perfectly normal configuration. Anyone can drop a file in, nobody can take one out.
The ten seconds granted to WebDAV
Since version 3.1.0, released in April 2026, HFS supports WebDAV, the protocol that lets you connect a shared folder as a network drive in Windows File Explorer or the Mac Finder. Some WebDAV clients behave in a peculiar way: the Finder, for example, first creates an empty file and immediately overwrites it with the real content. But overwriting a file requires permission to delete, and an "upload only" folder does not grant it.
So as not to break these clients, HFS gives whoever has just uploaded a file with a recognized WebDAV client ten seconds to replace it, even without delete permission. It is a reasonable compromise. The question is how HFS decided who "whoever has just uploaded" was.
Every anonymous visitor has the same name
The temporary permission was stored under a key made of two parts: the file's path and the user's name. For registered users that works, and there was even an automated test checking that two different accounts could not use each other's permission. But for someone who has not logged in, HFS returns an empty string as the name. Every anonymous visitor had the same name, and therefore the same key.
A second anonymous visitor, without the first one's cookies or credentials, could therefore send a PUT request to the same path within ten seconds, with a user agent containing the word WebDAV. HFS found the permission, deleted the existing file before even processing the new upload, and skipped both the can_delete check and the protection that ties anonymous uploads to the session that made them. If the new upload succeeded the file was replaced; if it failed or was interrupted, the original was deleted anyway. The force_webdav_login option, on by default, did not block this request, because a plain PUT with no WebDAV-specific headers was not treated as a WebDAV authentication request. Here too the catalogue speaks of CWE-863, incorrect authorization.
How serious it is
The conditions are narrow. You need a folder with anonymous upload and deletion forbidden, a file just uploaded by a WebDAV client, knowledge of its path, and a second request within ten seconds. Older files cannot be reached. The severity is moderate.
But inside that window the effect is concrete: a file that had just been published, perhaps already shared with others through its address, could be replaced by anyone with different content, at the same address. In a drop-off folder, that means the document you received might not be the one the sender sent.
How we verified it
Liam traced the path through the code of HFS 3.3.4 (commit bb6f718) and confirmed that the same code was still present on the main branch. He then built the server and ran it in a Docker container with no network access, with an uploads folder configured to allow uploads and forbid deletion. A first anonymous session uploaded a file containing first using the user agent of the Windows WebDAV client. A second session, with no cookies, tried to delete it and HFS correctly answered 403. That same second session then sent a PUT to the same path containing second: HFS answered 200, and the file at the original address contained second.
Both requests came from the same IP address, but the key contains neither the session nor the IP: the code indicates the result would be identical from different addresses, although we did not test that case separately. The advisory lists every version from 3.1.0 to 3.4.0 as affected.
The fix in 3.4.1
Version 3.4.1, released on 9 November, lists a single change in its release notes, in commit 9a33df2: the temporary permission is now tied to the username or, for anonymous visitors, to their session ID. If there is neither, no permission is granted. It is exactly the fix we had proposed. The new automated test checks both sides: another anonymous session gets 403, while the uploader can still overwrite the file the way the Finder does.
What to do if you use HFS
Update to 3.4.1 or later. HFS has an automatic update process, available even when it runs as a service, so the operation is usually quick. If for some reason you cannot update right away and you have folders with anonymous upload, consider requiring an account to upload, at least until the fix is installed.
More generally, a folder open to anonymous uploads is an open door by definition. It makes sense in many cases, but it is better used to receive files than to publish them to others, and its contents are worth checking regularly.
The same mistake, twice
Semaphore checked that the user could work in the project in the URL, then modified an object chosen by a number in the request. HFS checked that the file had been uploaded by the same user, but used a name that was identical for every anonymous visitor. In both cases the check was there, written with care, and in one case even covered by a test. What was missing was the right question: is the identity I am checking really that of the person, and the object, I am about to act on?
These mistakes do not show up when you use the application normally. They show up when you follow a value from the request to the point where it is used, asking each time "who decided this value?". That is part of the work we do.
Why we do research on open source software
Tools like Semaphore and HFS are the reason a small team can manage dozens of servers or exchange files without depending on a cloud service: open source, free, easy to install. But the more widely an application is used, the more its internal checks matter. Reading the code of the projects we use and recommend, and reporting what we find, is our way of giving something back to these projects and of knowing what we install for our clients.
If you use automation tools, file servers or applications with several users and roles, and you want to know whether the permissions really hold, we'd be glad to talk. Our cybersecurity work includes protecting and monitoring networks, incident response, audits, hardening and penetration tests, and it always starts here: reading the code and the configuration, finding where a check is missing, and telling you precisely what we verified and what we didn't.
HAVE A PROJECT?
Let's discuss how we can help your business with custom digital solutions.
CONTACT US