serveFileMove authorized config files with content verbs — the destination as ActionCreate, a .zddc source as ActionWrite — so a caller holding only create/write authority could plant or relocate an attacker-controlled .zddc / .zddc.zip cascade (admins:/acl:) that PUT and DELETE both gate behind ActionAdmin (VerbA / IsConfigEditor). The MOVE destination rides in the X-ZDDC-Destination header, which no dispatch gate inspects, so the bar must be enforced at the handler on the resolved target path. Centralize the escalation in configWriteAction() (.zddc / .zddc.zip → ActionAdmin, case-insensitive) and apply it to BOTH sides of serveFileMove; replace the inlined `.zddc` checks in serveFilePut/serveFileDelete with the same helper (also escalating whole-file .zddc.zip writes at the handler layer, where previously only the dispatch visibility gate covered them). Found via an authz-subsystem audit; the existing suite did not pin this path. Adds TestFileAPI_MoveOntoConfigRequiresConfigEdit (non-editor MOVE onto/away-from config → 403; config-editor → 200). Full Go suite + vet green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
37 lines
1.7 KiB
Go
37 lines
1.7 KiB
Go
package handler
|
|
|
|
import (
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"codeberg.org/VARASYS/ZDDC/zddc/internal/apps"
|
|
"codeberg.org/VARASYS/ZDDC/zddc/internal/policy"
|
|
)
|
|
|
|
// configWriteAction returns the action a write to absPath must be authorized
|
|
// as. The .zddc cascade file and the .zddc.zip bundle are policy, not content:
|
|
// mutating either is a VerbA operation requiring standing config-edit authority
|
|
// (IsConfigEditor — a subtree admin or `a`-verb holder, no elevation), which
|
|
// the decider enforces when the action is tagged ActionAdmin. For any other
|
|
// path the supplied default action is returned unchanged.
|
|
//
|
|
// This is the single predicate behind the per-verb escalation that previously
|
|
// lived inlined in serveFilePut/serveFileDelete (.zddc only) and was MISSING
|
|
// from serveFileMove — letting a MOVE plant or relocate a policy file with mere
|
|
// create/write authority. PUT/DELETE on a URL-visible .zddc.zip are also
|
|
// existence-gated to config-editors at dispatch (the bundle visibility gate in
|
|
// cmd/zddc-server), but a MOVE destination rides in the X-ZDDC-Destination
|
|
// header and never reaches that gate — so the authority bar must be enforced
|
|
// here, on the resolved target path, for every write verb.
|
|
//
|
|
// Matching is case-insensitive to align with HasReservedSidecar: ZDDC_ROOT may
|
|
// sit on a case-insensitive filesystem (SMB/CIFS/Azure Files) where `.ZDDC` /
|
|
// `.ZDDC.ZIP` resolve to the same files, and a case-varied target must not slip
|
|
// past the gate.
|
|
func configWriteAction(absPath, def string) string {
|
|
base := filepath.Base(absPath)
|
|
if strings.EqualFold(base, ".zddc") || strings.EqualFold(base, apps.BundleName) {
|
|
return policy.ActionAdmin
|
|
}
|
|
return def
|
|
}
|