F03 | CONFIRMED | src/server/v2.0/handler/notification_policy.go:130 | Update authorizes on the URL project but takes the target row from the body: `lib.JSONCopy(policy, params.Policy)` maps `id` -> Policy.ID (pkg/notification/policy/model/model.go:16 `json:"id"`), then `policy.ProjectID = projectID` and `webhookPolicyMgr.Update` -> dao.Update -> `ormer.Update(policy)` (pkg/notification/policy/dao/dao.go:88) writes every column by PK; params.WebhookPolicyID is never used.
F06 | CONFIRMED | src/controller/artifact/controller.go:586 | `err = c.labelMgr.AddTo(ctx, labelID, artifactID)` with no label lookup; manager AddTo only inserts a reference row (pkg/label/manager.go:95-104) and the DAO's only guard is a foreign-key existence check (pkg/label/dao/dao.go:166), and the handler (server/v2.0/handler/artifact.go:459) passes params.Label.ID unvalidated — a project-scoped label from another project can be attached.
F09 | CONFIRMED | src/server/middleware/contenttrust/cosign.go:56 | The policy is satisfied by mere presence of an accessory row: `for _, acc := range art.Accessories { if acc.GetData().Type == model.TypeCosignSignature {...} }`, and that row is created with no cryptographic verification at all — server/middleware/cosign/cosign.go:72-115 creates it whenever the push path matches `sha256-<digest>.sig` and any referenced layer has mediaTypeCosignLayer.
F12 | CONFIRMED | src/server/v2.0/handler/scan.go:102 | `bytes, err := s.scanCtl.GetScanLog(ctx, params.ReportID)` — the handler resolves the artifact at :98 but never compares it to the report; GetScanLog (controller/scan/base_controller.go:635-641) resolves tasks purely from the UUID with no artifact/project binding.
F15 | CONFIRMED | src/server/v2.0/handler/retention.go:326 | `tasks, err := r.retentionCtl.ListRetentionExecTasks(ctx, params.Eid, query)` after requireAccess was evaluated on the policy fetched from params.ID at :318 — Eid is never checked to belong to that policy (same for GetTotalOfRetentionExecTasks at :330).
F18 | CONFIRMED | src/server/v2.0/handler/immutable.go:62 | `ia.immuCtl.DeleteImmutableRule(ctx, params.ImmutableRuleID)` after authorizing only on the URL project; the controller passes the id straight through (controller/immutable/controller.go:49-51) and the DAO deletes by PK alone (pkg/immutable/dao/dao.go:119-126) — note UpdateImmuRule does pass projectID and the controller checks it, so the omission is delete-specific.
F21 | CONFIRMED | src/controller/registry/controller.go:91 | `url, err := lib.ValidateHTTPURL(registry.URL)` — that function (lib/endpoint.go:26-43) only trims, defaults the scheme and rejects non-http(s), with the comment "To avoid SSRF security issue, refer to #3755"; no loopback/link-local/RFC1918 filtering, and :97 `c.IsHealthy` performs the request and surfaces status+error (controller.go:168-178). Severity/gating caveat in the claim is accurate: the endpoint is system-admin gated at server/v2.0/handler/registry.go:45.
F24 | CONFIRMED | src/server/v2.0/handler/notification_policy.go:155 | `policy, err := n.webhookPolicyMgr.Get(ctx, params.WebhookPolicyID)` — authorization is on the URL project only, Get resolves by PK with no project filter (pkg/notification/policy/manager.go:93), and the payload includes the target auth header (server/v2.0/handler/model/notification_policy.go:37 `AuthHeader: t.AuthHeader`).
F27 | CONFIRMED | src/controller/artifact/processor/chart/chart.go:87 | `content, err := ioutil.ReadAll(blob)` with no size limit, then `p.chartOperator.GetDetails(content)` which calls helm `loader.LoadArchive(bytes.NewReader(content))` (pkg/chart/operator.go:91) — full in-memory decompression of a pushed blob; severity Medium looks fair given it needs an authenticated addition read.
F30 | CONFIRMED | src/server/middleware/blob/put_manifest.go:37 | `body, err := ioutil.ReadAll(r.Body)` preceded only by `lib.NopCloseRequest(r)`, which itself buffers the whole body via `io.Copy(&buf, body)` (lib/request.go:43-55); grep finds no http.MaxBytesReader or io.LimitReader anywhere under server/ or lib/, and the size check would come from UnmarshalManifest only after the read.
F33 | CONFIRMED | src/server/v2.0/handler/notification_policy.go:143 | `n.webhookPolicyMgr.Delete(ctx, params.WebhookPolicyID)` after RequireProjectAccess on the URL project; manager.Delete -> dao.Delete removes by PK with no project predicate (pkg/notification/policy/dao/dao.go:122-127).
F36 | CONFIRMED | src/server/v2.0/handler/robot.go:287 | updateV2Robot authorizes with attacker-supplied values — `rAPI.requireAccess(ctx, params.Robot.Level, params.Robot.Permissions[0].Namespace, rbac.ActionUpdate)` — while the row being edited came from params.RobotID (:189); DeleteRobot/GetRobotByID/RefreshSec correctly use `r.Level, r.ProjectID` (:206, :180, :225). The claim's own caveat is accurate: :291 `params.Robot.Name != r.Name` still requires the victim robot's name.
F39 | CONFIRMED | src/server/middleware/repoproxy/proxy.go:80 | `setHeaders(w, size, "", art.Digest)` runs after `io.CopyN(w, reader, size)` at :73 has already committed the response; setHeaders (:192-200) sets Content-Length, Docker-Content-Digest and Etag, all of which are discarded once the body is written. Severity note: the claim's own "not directly exploitable" framing matches the code.
F42 | CONFIRMED | src/server/v2.0/handler/retention.go:277 | `r.retentionCtl.OperateRetentionExec(ctx, params.Eid, params.Body.Action)` while requireAccess at :273 was computed from the policy fetched with params.ID at :269 — Eid is never validated against that policy.
F45 | CONFIRMED | src/server/v2.0/handler/notification_policy.go:155 | Same code as F24: `webhookPolicyMgr.Get(ctx, params.WebhookPolicyID)` is PK-only (pkg/notification/policy/manager.go:93) and ToTargets returns AuthHeader (handler/model/notification_policy.go:37), so any project with webhook-read enumerates other projects' policy IDs; note this is a duplicate of F24.
F48 | CONFIRMED | src/server/v2.0/handler/model/scanner.go:43 | `AccessCredential: s.AccessCredential` is returned by GetScannerOfProject (server/v2.0/handler/project.go:581), gated only by rbac.ResourceScanner/ActionRead, which the lowest project role holds (common/rbac/project/rbac_role.go:323 in the "limitedGuest" policy block); the value is never decrypted or masked on the read path (pkg/scan/scanner/manager.go:93 -> scanner.GetRegistration).
F51 | CONFIRMED | src/server/middleware/vulnerable/vulnerable.go:110 | `if art.IsImageIndex() { ... skippingAllowlist := []string{image.ArtifactTypeImage, cnab.ArtifactTypeCNAB} ... return nil }` returns before both the IsScanSuccess check (:119) and the severity judgement (:126), so a scanned multi-arch image of type IMAGE/CNAB is never blocked. Two caveats: it is an explicit, commented carve-out rather than an oversight, and an index with no report at all is still rejected by the NotFound branch at :99, so "every multi-arch image" is slightly overbroad — the bypass of the severity gate itself is real.
F54 | CONFIRMED | src/server/v2.0/handler/preheat.go:581 | `execution, err := api.executionCtl.Get(ctx, params.ExecutionID)` after RequireProjectAccess on params.ProjectName only — the execution is never checked against the project's preheat policy, unlike ListExecutions which constrains `vendor_id = policy.ID` (:614-618).
F57 | CONFIRMED | src/server/v2.0/handler/scan.go:102 | Same defect as F12: `s.scanCtl.GetScanLog(ctx, params.ReportID)` is keyed on the UUID alone (controller/scan/base_controller.go:640 `vuln.ParseReportIDs(uuid)`), with the artifact resolved at :98 and then discarded; duplicate of F12 at a different severity.
