* fix(replication): preserve artifact type for multi-tier accessories
In getAccessoryArts, reassigning art when fetching accessory details
shadowed the parent artifact and caused sub-accessories (such as signatures
of an SBOM) to inherit Type "UNKNOWN" from the accessory artifact rather
than the subject artifact's type (e.g. "IMAGE"). Consequently, when a
replication policy configured a resource filter (e.g. "image"), the
sub-accessories were dropped by the artifact type filter.
Fix this by using a separate variable subArt to avoid shadowing art,
and propagate art.Type to subArt so nested accessories inherit the
root subject artifact's type.
Signed-off-by: stonezdj <stonezdj@gmail.com>
* test(replication): cover nested accessory type inheritance
Signed-off-by: stonezdj <stonezdj@gmail.com>
---------
Signed-off-by: stonezdj <stonezdj@gmail.com>
Move the RBAC evaluator from github.com/casbin/casbin v1.9.1 to
github.com/casbin/casbin/v3 v3.11.0. v1 is unmaintained and pulls in the
archived Knetic/govaluate.
v3 returns errors from NewEnforcer, Enforce and policy loading; a
failure now denies the request and is logged. v3 pre-registers
keyMatch2 and never replaces it, so Harbor's matcher is registered as
harborKeyMatch2. v3 parses policy lines as CSV, so the adapter loads
rules as arrays to keep names with quotes or commas working.
Signed-off-by: Prasanth Baskar <prasanth@8gears.com>
The resource filter stopped filtering artifacts by type when the chart
resource type was removed in #18191, so policies with the image filter
replicated charts and other artifacts as well.
Fixes#24047
Signed-off-by: chlins <chlins.zhang@gmail.com>
The adapter is built without -ldflags, so its startup log and
/api/v1/metadata report version "dev", commit "none" and build date
"unknown". Pass the version, commit and date the same way the
upstream release build does.
Signed-off-by: YoungjuKim <fjvbn2003@gmail.com>
Use the established “Tag Immutability” label for the robot-account permission selector in all supported locales.
Fixes#24170
Prepared and tested with Codex assistance; no human review is claimed.
Signed-off-by: NUAAwqy <146524024+NUAAwqy@users.noreply.github.com>
test(robot): expect inherited signature for unsigned index child in replication event test
Commit c3ec2b133e introduced signature inheritance for OCI index children (#23540),
marking unsigned child artifacts under a signed index with the 'signed-inherited' status
in the Portal UI instead of 'color-red' (unsigned).
In 'Test Case - Replication Triggered By Events', image2 (busybox) under the signed
index was asserted with 'Should Not Be Signed', which looked for 'color-red' icon and
failed. Add 'Should Be Inherited Signed' keyword and update the test case to assert
the inherited signature state on image2 under the signed index.
Signed-off-by: stonezdj <stonezdj@gmail.com>
Update guest radio checkbox xpath locator in Project-Members_Elements.robot to match the current DOM structure.
Signed-off-by: stonezdj <stonezdj@gmail.com>
fix(webhook): default webhook egress policy level to allow_all
When the webhook egress policy is empty or unset, default to egressAllowAll
to preserve Harbor's default egress behavior and avoid regressions for internal
and test webhook targets.
Signed-off-by: stonezdj <stonezdj@gmail.com>
- Record and bubble up failure error messages (including tail of job logs) in Jobservice
- Store status_message on failed task and aggregate task errors into execution status_message
- Expose status_message in swagger ReplicationTask definition and handler
- Display status_message tooltip on replication tasks and execution status in UI portal
Signed-off-by: stonezdj <stonezdj@gmail.com>
feat(replication): separate event-triggered replication into dedicated queue
Separate replication tasks generated by event-based executions into
a dedicated EVENT_REPLICATION queue in JobService with higher priority
to prevent head-of-line blocking by bulk manual or scheduled replication.
Signed-off-by: stonezdj <stonezdj@gmail.com>
Co-authored-by: Wang Yan <wangyan_0219@hotmail.com>
* fix(jobservice): prevent CPU spin from unreachable cron expressions
Cron expressions like '0 0 3 30 2 *' (Feb 30) parse successfully because
robfig/cron validates each field individually without cross-checking day-of-month
against month. schedule.Next() then returns time.Time{} since no matching date
exists within the five-year search window. This causes three failures:
1. ValidateCronString accepts the expression. Fix: call schedule.Next after Parse
and reject zero results.
2. The scheduleNextJobs enqueuer loop spins indefinitely. Fix: add !t.IsZero()
guard to the loop condition.
3. clearDirtyJobs misses negative-epoch entries. Fix: use math.MinInt64 as the
lower bound instead of 0.
Fixes#23924
Signed-off-by: Nikita Kodkany <nikitakodkany@gmail.com>
* fix(jobservice): address review feedback on unreachable cron fix
Signed-off-by: Nikita Kodkany <nikitakodkany@gmail.com>
* fix(jobservice): address review feedback on unreachable cron fix
- Log and set lastEnqueueErr when policy is skipped as unreachable
- Add IsCronReachable check in jobservice Policy.Validate and controller
submit validation so pre-upgrade schedules are rejected at restore
- Fix probe comment: century year gap, not weekday intersection
- Collapse cronIsReachable/IsCronReachable into one exported function
- Use consistent error message across all validation sites
- Fix clearDirtyJobs comment to describe legacy cleanup
- Improve test robustness: FailNow on timeout, policy-specific assertion
- Add Feb 29 leap day test case to pin reachable behavior
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Nikita Kodkany <nikitakodkany@gmail.com>
* fix(jobservice): stop the enqueuer loop when Next returns zero mid-loop
The zero-time check only covered the first schedule.Next call. A reachable
spec can still return time.Time{} later: for "0 0 0 29 2 *", Next after
2096-02-29 finds nothing because 2104-02-29 is past robfig/cron's five-year
search window, and time.Time{}.Before(horizon) keeps the loop spinning.
The loop now lives in fireTimes with the IsZero guard in its condition. The
warning for a zero first Next now says there is no occurrence within the
search window, because in 2097-2099 that also applies to valid Feb 29 specs.
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* fix(jobservice): use dedicated enqueuer in test to fix data race
The test goroutine called scheduleNextJobs on the suite-level enqueuer
while its background loop was running, racing on lastEnqueueErr.
Signed-off-by: Nikita Kodkany <nikitakodkany@gmail.com>
* style(jobservice): gofmt enqueuer_test.go
Signed-off-by: Vadim Bauer <vb@container-registry.com>
---------
Signed-off-by: Nikita Kodkany <nikitakodkany@gmail.com>
Signed-off-by: Vadim Bauer <vb@container-registry.com>
Co-authored-by: Nikita Kodkany <nkodkany@crusoe.ai>
Co-authored-by: Nikita Kodkany <nikitakodkany@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Vadim Bauer <vb@container-registry.com>
List v2.15.x, v2.14.x and v2.13.x, the three most recent minor
releases, in line with the support policy stated in the same file.
Fixes#24059
Signed-off-by: Prasanth Baskar <prasanth@8gears.com>
Clean up empty secrets when creating a new secret handler in registryctl
and skip empty secrets during validation to prevent authentication bypass
when JOBSERVICE_SECRET is empty/unset.
Signed-off-by: stonezdj <stonezdj@gmail.com>
Co-authored-by: Pushkar Joglekar <pushkar.joglekar@broadcom.com>
Redact the URL field in ListScannerCandidatesOfProject response to prevent
non-admin project members from listing system-global scanner URLs, while
retaining the scanner URL when the caller has system admin permissions.
Signed-off-by: stonezdj <stonezdj@gmail.com>
In basicController.Stop, the execution query previously used artifact.Digest
without scoping the lookup to the caller's artifact. Because artifact digests
are content-addressable and can be identical across different projects/tenants,
this allowed users to stop scans belonging to other projects, introducing a
cross-tenant scan-stop (availability) vulnerability.
This commit scopes the execution query specifically to the artifact's unique
database ID (`artifact.ID`), ensuring that only the scan of the caller's specific
artifact is stopped.
Signed-off-by: stonezdj <stonezdj@gmail.com>
Filters with several comma-separated values are stored wrapped in braces
(e.g. {a,b}). When opening such a policy for edit, the brace-stripping
branch returned from the forEach callback instead of assigning the value,
so the repository, tag and label fields stayed empty.
The fix was originally proposed in #23323, which was closed
as stale without review; this re-submits it with a regression test.
Fixes#23293
Signed-off-by: Viktor Erpylev <velmoga@gmail.com>
Co-authored-by: Psykii22 <189542486+Psykii22@users.noreply.github.com>
feat(portal): custom project roles UI
Frontend for the custom project roles feature (backend: #23804). Splits the
UI out of the combined feature branch and adapts it to the final backend API.
- Roles management: list/create/edit custom roles with a permissions panel
(left-side-nav/roles/*, shared/components/role-permissions-panel/*), wired
into routing (base.module, harbor-shell nav, shared.const) behind the
system-admin guard.
- Member add/edit (member, add-member, add-group): dynamic role selector that
lists built-in and custom roles from the role API, with a built-in badge.
- Robot account editor: fetch creator's effective project permissions from
/users/current/permissions (scope=/project/{id}, relative) instead of the
removed /projects/{name}/permissions/effective endpoint, to filter grantable
robot permissions; fail closed on empty permissions.
- Roles grid: compact fixed widths for ID and built-in columns, multi-row selection,
ascending ID sorting by default, and proper navigation label.
- Role pickers: shared getAllRoles() helper across member and group dialogs with
loading state and validation against unlisted roles.
- Role wizard: exact role name uniqueness check, input validation, and full-width modal.
- i18n: ROLE.* keys and MEMBER.BUILT_IN synced and translated across all supported locales.
- Tests: unit test coverage for role permissions panel, roles component, add-role wizard,
and role pagination helper.
Signed-off-by: Max Grau Stenzel <max.grau.stenzel@gmail.com>
Signed-off-by: Prasanth Baskar <prasanth@8gears.com>
Signed-off-by: stonezdj <stonezdj@gmail.com>
Co-authored-by: Prasanth Baskar <prasanth@8gears.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
- Bump trivy adapter version to v0.39.1 (ref: #24080)
- Bump distribution version to v2.8.3-harbor.2 (ref: #23991)
(cherry picked from commit 0c7f91025d)
Signed-off-by: stonezdj <stonezdj@gmail.com>
Translate the strings of the French portal file that were still in
English, mostly from recent features (audit log event types, Dragonfly
preheat scope, OIDC logout, preferences).
Signed-off-by: Lewis Bertin <lewis@tactiqual.tech>
* fix(registry): validate upload Location against the request origin, not the configured endpoint
The GHSA-wjpm-hv5w-gxr6 fix compared registry-provided upload Location
headers against the configured registry endpoint (c.url). Adapters whose
authorizer rewrites the request host break under that rule: AWS ECR is
configured with https://api.ecr.<region>.amazonaws.com but every request
is rewritten to <account>.dkr.ecr.<region>.amazonaws.com, which is also
the host in the Location ECR returns. Every push replication to ECR
failed with 'resolves to a different origin'.
The correct invariant is 'the Location must be same-origin with the
server that issued it'. Validate the Location when it is received, using
the URL of the request that produced the response (after the authorizer
ran) as the origin. Root-relative Locations are still appended to the
endpoint so a path prefix is preserved. Cross-origin, network-path and
userinfo-bearing values are still rejected and the attacker host is
never contacted.
Fixes the ECR push replication regression introduced by #<GHSA-wjpm PR>.
Signed-off-by: wang yan <yan-yw.wang@broadcom.com>
* fix(registry): resolve upload Location against the final request URL after redirects
c.client.Do follows redirects, but req.URL still refers to the original
request. A same-origin 307 to another path followed by a path-relative
Location would resolve against the original path and send the next
upload to the wrong URL. Use resp.Request.URL as the origin when the
HTTP client followed redirects, but reject a redirect that changed the
origin so an arbitrary redirect target can never become a trusted,
credential-bearing upload destination.
Signed-off-by: wang yan <yan-yw.wang@broadcom.com>
* fix(registry): simplify upload Location comments
Signed-off-by: wang yan <yan-yw.wang@broadcom.com>
---------
Signed-off-by: wang yan <yan-yw.wang@broadcom.com>
Three dependabot bumps landed on release-2.15.0 but never reached main:
- github.com/jackc/pgx/v5 v5.10.0 -> v5.11.0 (#23902)
- github.com/aws/aws-sdk-go-v2/credentials v1.19.19 -> v1.20.3 (#23908)
- github.com/go-openapi/runtime v0.32.2 -> v0.33.2 (#23909)
The transitive sets follow from the module graph. main is behind
release-2.15.0 on the aws-sdk core, so the aws side pulls more here than
it did on the release branch. go-openapi/runtime 0.33 moves spec,
validate, analysis and jsonpointer to 1.0.0 and brings along strfmt,
loads, the swag modules, otel and testify.
Signed-off-by: Prasanth Baskar <prasanth@8gears.com>
fix(contenttrust): allow legacy signer pull by default to avoid regression
Default CONTENT_TRUST_LEGACY_SIGNER_PULL_ENABLED to true to prevent breaking
existing cosign and notation signing workflows on upgrade.
Signed-off-by: wang yan <yan-yw.wang@broadcom.com>
The webhook SSRF guard was a single boolean. With it off (the default
since #24078) webhooks can reach loopback and cloud metadata; with it on,
every internal receiver breaks.
HARBOR_WEBHOOK_EGRESS_POLICY (harbor.yml webhook_egress.policy) selects a
level: allow_all, block_restricted (default) or public_only. Restricted
covers loopback, link-local, cloud metadata, unspecified, multicast and
NAT64/6to4/Teredo. HARBOR_WEBHOOK_EGRESS_ALLOWLIST takes IPs, CIDRs,
hostnames and *.domain suffixes that are allowed at every level.
When the policy is unset, the deprecated HARBOR_ALLOW_PRIVATE_NETWORK_ACCESS
keeps its 2.15 meaning: true is allow_all, false is public_only. Invalid
values fail closed, prepare rejects an unknown policy, and a blocked
target is logged with the setting that reopens it.
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* fix(webhook): allow private network access by default to avoid regression
Default HARBOR_ALLOW_PRIVATE_NETWORK_ACCESS to true to prevent breaking
existing on-premise and Kubernetes deployments where webhooks target
internal services.
Remove the allow_private_network_access field from harbor.yml to keep
the YAML schema clean, managing this toggle solely via the environment
variable.
Signed-off-by: wang yan <yan-yw.wang@broadcom.com>
* resolve copilot review
Signed-off-by: wang yan <yan-yw.wang@broadcom.com>
* resolve the comments
Signed-off-by: wang yan <yan-yw.wang@broadcom.com>
---------
Signed-off-by: wang yan <yan-yw.wang@broadcom.com>
* fix(webhook): block SSRF to private-network targets and stop reflecting response bodies
Webhook and Slack notification jobs delivered to any operator-supplied URL with
no address classification, and folded a non-2xx response body into the
project-readable task log. A project admin could reach loopback, RFC1918,
link-local and cloud-metadata (169.254.169.254) targets and read internal
service responses back through the webhook task log (CWE-918).
Classify destinations against the IANA special-purpose registries and reject
non-public targets at (1) webhook policy validation and (2) a dial-time guard on
the notification HTTP clients; drop the response body from the task-log error;
refuse redirects. The HARBOR_ALLOW_PRIVATE_NETWORK_ACCESS opt-out (default off)
restores delivery to internal endpoints for operators who rely on it.
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* test(webhook): cover SSRF target rejection, body-drop and redirect refusal
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* test(webhook): resolve upstream's mixed-case target host in the stub resolver
Upstream #23889 added a create-policy test that posts a mixed-case target
address and expects 201. With the SSRF target check in place that hostname
now goes through the stub resolver, which only knew hooks.example.com, so
the test failed with 400. Teach the stub the second hostname; the upstream
test keeps asserting lowercasing, the SSRF tests keep asserting rejection.
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* fix(webhook): honour the jobservice proxy settings in the SSRF-safe transport
The public-network guard cleared Transport.Proxy, so webhook and Slack
delivery ignored the HTTP_PROXY/HTTPS_PROXY/NO_PROXY settings that
prepare writes for jobservice. Deployments without a direct Internet
route lost every public webhook.
Keep the transport's proxy function. When it selects a proxy, validate
the target host (literal or all resolved addresses) as public before the
request is handed to the proxy, and let only dials to that proxy address
skip the dial-time private-address check. When no proxy applies
(NO_PROXY, loopback), the resolved-address dial guard is unchanged. A
direct request aimed at the proxy's own address is refused so it cannot
inherit the proxy exemption.
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* chore: describe the fix without advisory identifiers
Signed-off-by: Vadim Bauer <vb@container-registry.com>
(cherry picked from commit 54cc3fca7a741ecb31293a767b998ceffec9d5b5)
* fix(webhook): drain notification response bodies before closing
Closing an unread response body forces the transport to drop the
keep-alive connection. Discard up to 64KiB of the body (never surfacing
it) so the pooled connection is reused after non-2xx responses.
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* test(webhook): assert the dial guard on production clients and pin the env
Exercise the production notification clients against a loopback target
so dropping the public-network guard fails a test, cover connection
reuse after non-2xx responses, and pin HARBOR_ALLOW_PRIVATE_NETWORK_ACCESS
to false in tests that assert the default blocking behaviour.
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* fix(prepare): keep network.allow_private_network_access across migrate
The migrate command re-renders harbor.yml from the 2.15.0 template, so a
key missing there is dropped. Render the network block, preserving an
operator-set value and defaulting to false.
Signed-off-by: Vadim Bauer <vb@container-registry.com>
---------
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* fix(policy): stop User-Agent from bypassing content-trust and vulnerability policies
SkipPolicyChecking exempted a caller from the content-trust and
vulnerability-prevention pull policies when it held push and the request
User-Agent contained "cosign" or "notation". User-Agent is fully
attacker-controlled (CWE-807), so any push-capable principal (e.g. the
Developer role) bypassed both policies on a plain manifest pull by setting
one header.
Remove the User-Agent discriminator. Scanner pull keeps its bypass via the
non-forgeable scanner-pull RBAC action, and a genuinely signed artifact is
still recognised by its signature accessory. The pull-before-first-signature
bootstrap for content-trust is preserved as an opt-in, content-trust-only
exemption (CONTENT_TRUST_LEGACY_SIGNER_PULL_ENABLED=true); it never affects
the vulnerability-prevention policy.
Reported by sabuhimammadov0 (GHSA-wrw5-gmvj-gf23); independently reported on
the cncf-harbor-security list by kt (thread #61), Oleh Konko (thread #25) and
Eren Akdag (thread #68).
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* test(policy): spoofed User-Agent no longer bypasses pull policies
Cover GHSA-wrw5-gmvj-gf23: a push-capable v2token caller with
User-Agent: cosign is now blocked by both the content-trust and the
vulnerability-prevention middleware, while scanner-pull and genuine signature
accessories still bypass. Add a test for the opt-in content-trust bootstrap.
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* test(contenttrust): pin the legacy signer-pull hatch off in the spoofed User-Agent test
The test asserted the hatch-off behaviour but inherited the process
environment, so it failed whenever the flag was exported.
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* test(vulnerable): enable the legacy signer-pull hatch in the spoofed User-Agent test
With the flag off the test could not detect the User-Agent bypass
being moved back into the shared policy check behind the flag.
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* chore: describe the fix without advisory identifiers
Signed-off-by: Vadim Bauer <vb@container-registry.com>
(cherry picked from commit 49f78850621446dd113a3d2517fed7e23f3e565b)
* test(contenttrust): cover the legacy signer-pull hatch guards
Add negative controls for the opt-in signer-pull exemption: it stays off
when the variable is unset, and with it enabled a pull is still rejected
without push permission or without a signing-client User-Agent. Also
cover the notation User-Agent path.
Signed-off-by: Vadim Bauer <vb@container-registry.com>
---------
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* test(pkg/registry): regression for chunk-upload Location SSRF (GHSA-wjpm-hv5w-gxr6)
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* fix(registry): validate chunk-upload Location against the registry origin (GHSA-wjpm-hv5w-gxr6)
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* fix(registry): keep the endpoint path prefix for root-relative upload Locations
Distribution with relativeurls returns /v2/... without the path prefix the endpoint is served under. Resolving it against the endpoint dropped that prefix, so pushes through a path-prefixed registry endpoint hit the wrong route. Append root-relative paths to the endpoint as before and keep the origin and userinfo checks.
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* chore: describe the fix without advisory identifiers
Signed-off-by: Vadim Bauer <vb@container-registry.com>
(cherry picked from commit 6b4be4734a401c5b427667544534aea8bed53c1d)
---------
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* test(common/http): regression for pagination Link SSRF (GHSA-xgp2-5vxg-rgh2)
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* fix(common/http): pin pagination Link next URL to the request origin (GHSA-xgp2-5vxg-rgh2)
GetAndIteratePagination built the next-page URL by string-concatenating the
attacker-controlled Link rel="next" value onto scheme+host and re-parsing it, so
a value like "@evil.example/x" re-pointed the request at evil.example (host) with
the original host demoted to userinfo. Because Client.Do re-runs its auth modifiers
on every hop, the configured registry credentials were then sent to that host
(SSRF + credential disclosure).
Resolve the Link as an RFC 3986 reference against the current page URL, reject
userinfo, and require the resolved scheme/host/port to equal the origin. Relative
same-origin pagination keeps working; any cross-origin next target fails closed.
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* test(common/http): require cross-origin pagination links to fail with an error
The regression test discarded the pagination error, so an implementation that silently stopped at a cross-origin Link and returned a truncated list would still pass. Assert that every vector returns an error.
Signed-off-by: Vadim Bauer <vb@container-registry.com>
(cherry picked from commit bfa9a1e750cceb9363a510d548f14694fdf6ae31)
* chore: describe the fix without advisory identifiers
Signed-off-by: Vadim Bauer <vb@container-registry.com>
(cherry picked from commit 00e1558ff2fd848ebd071d21a9a9b3e1895df761)
* test(common/http): cover userinfo rejection and absolute same-origin pagination links
The existing userinfo vector is rejected by url.Parse before the
userinfo check runs, so add resolveNextLink cases that only that check
can reject. Also exercise absolute same-origin next links, default port
equivalence and scheme/port mismatches.
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* fix(registry): keep catalog and tag list pagination on the registry origin
Catalog and ListTags followed the Link rel="next" value from the remote
registry verbatim when absolute, and appended it to the endpoint otherwise,
so an absolute URL to another host or a value starting with "@" sent the
next request elsewhere. Authorizers that attach credentials regardless of
host (basic auth used by the JFrog adapter, the ACR token authorizer) then
disclosed them to that host.
Build the next URL exactly as before and reject it unless it keeps the
endpoint's scheme, host, effective port and userinfo. SameOrigin is
exported from common/http to share the origin check.
Signed-off-by: Vadim Bauer <vb@container-registry.com>
---------
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* fix(security): fail closed when auth mode lookup fails (GHSA-3x7c-wf86-mjx6)
The security context middleware called config.AuthMode and, on error, logged a
warning and continued processing the request with no auth mode in the request
context. Determining the request's security context is an authorization decision
that depends on the configured auth mode, so a lookup failure must reject the
request rather than fall through to the handler (CWE-636).
Reject with 500 via lib_http.SendError before the security-context generators
run. Normal requests, where the lookup succeeds, are unaffected.
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* test(security): assert middleware fails closed on auth mode lookup error (GHSA-3x7c-wf86-mjx6)
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* chore: describe the fix without advisory identifiers
Signed-off-by: Vadim Bauer <vb@container-registry.com>
(cherry picked from commit 33304265ce4ad826c63e471fd65bcbf2aa589c86)
* test(security): restore default config manager after TestSecurity
Signed-off-by: Vadim Bauer <vb@container-registry.com>
---------
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* fix(authproxy): stop a local sysadmin flag leaking into an authproxy context (GHSA-xvh7-74g6-29xw)
In auth_mode=http_auth the /v2 path loads the Harbor-local user record by name
and overlays only GroupIDs and AdminRoleInAuth from the TokenReview result
(server/middleware/security/auth_proxy.go). The local SysAdminFlag survives, and
local.SecurityContext.IsSysAdmin returns SysAdminFlag || AdminRoleInAuth, so an
external identity that the authproxy admin policy did not grant admin obtains
sysadmin /v2 authorization purely by colliding with a local sysadmin name -- the
seeded admin account always exists.
Clear SysAdminFlag on the overlaid record so AdminRoleInAuth, derived from the
TokenReview username and groups against the configured admin set, is the only
admin signal in authproxy mode. An explicitly allowlisted authproxy admin is
unaffected: its access comes from AdminRoleInAuth, not a local flag.
Reported by cyberlanc3r (GHSA-xvh7-74g6-29xw); independently reported by
Mitchell Benjamin (thread #49, topic 119514428).
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* test(authproxy): regression for sysadmin-flag collision (GHSA-xvh7-74g6-29xw)
Drive the real authProxy.Generate + local.SecurityContext.IsSysAdmin with a
mocked user manager returning a colliding sysadmin-flagged local record. Assert
the external identity is not sysadmin with an empty admin allowlist, and that an
explicitly allowlisted authproxy admin still is.
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* chore: describe the fix without advisory identifiers
Signed-off-by: Vadim Bauer <vb@container-registry.com>
(cherry picked from commit df6273ae8d0dd15a0a1f2f84d5a9b3f5b018bdee)
* test(authproxy): return a fresh user per lookup in the collision test
The mocked GetByName returned one shared *User, and Generate mutates it,
so the allowlisted arm saw a record whose sysadmin flag the first arm had
already cleared and never exercised the collision. Return a new record
per call, as the DB-backed manager does, and assert the allowlisted arm
derives admin from AdminRoleInAuth with the local flag cleared.
Signed-off-by: Vadim Bauer <vb@container-registry.com>
---------
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* fix(immutable): evaluate all repository and tag selectors (GHSA-27f2-qg57-2gwg)
The immutable tag rule matcher only read ScopeSelectors["repository"][0] and
TagSelectors[0], so a rule carrying more than one repository or tag selector
protected only the first entry. Pushes and deletes to the repositories or tags
named by the remaining selectors bypassed the immutability control.
Evaluate every selector in each dimension and match when any of them matches,
mirroring the OR/alternation the portal expresses as a single {a,b} doublestar
pattern. Single-selector rules, which is all the portal ever creates, keep
identical behaviour.
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* test(immutable): cover multi-selector immutable rule matching (GHSA-27f2-qg57-2gwg)
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* fix(immutable): subtract exclusion selectors instead of OR-ing them
Several selectors in one rule dimension must behave like the portal's
single {a,b} pattern. OR-ing every selector made two repoExcludes or
excludes selectors select everything, so excluded repositories and
tags became immutable. Inclusion selectors stay alternatives, every
exclusion selector now has to select the candidate, and multi-tag
candidates are evaluated per tag so tags excluded by different
selectors do not cancel out.
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* chore: describe the fix without advisory identifiers
Signed-off-by: Vadim Bauer <vb@container-registry.com>
(cherry picked from commit 27f4614d1ec7484eda1336bb2e01cf1b891326df)
* fix(immutable): ignore null selectors in rule matching
The rule API stores selector lists without validation, so a null entry
after the first selector was harmless while only the first one was read.
Evaluating every selector turned such entries into a nil dereference.
Drop null entries before matching.
Tests now purge leftover rules of their project before running, since
Match evaluates every rule of the project.
Signed-off-by: Vadim Bauer <vb@container-registry.com>
---------
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* fix(scan): verify TLS and bound body on scanner bearer token request (GHSA-884q-mjcg-86x4)
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* test(scan): assert bearer token request verifies TLS and bounds body (GHSA-884q-mjcg-86x4)
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* chore: describe the fix without advisory identifiers
Signed-off-by: Vadim Bauer <vb@container-registry.com>
(cherry picked from commit c9d2c8dc3997ef80fb1ea8a09936d01a13bab8e9)
* test(scan): assert certificate verification error type in bearer token TLS test
Match *tls.CertificateVerificationError via errors.As instead of a
substring of the error message, so the test does not depend on x509
error wording and cannot pass on an unrelated failure.
Signed-off-by: Vadim Bauer <vb@container-registry.com>
---------
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* fix(scanner): stop access_cred ORM filter oracle (GHSA-vwvv-675v-p6rr)
The scanner_registration.access_cred column stores the literal Authorization
value Harbor presents to a scanner adapter. It lacked the filter:"false" ORM
tag, so the generic q= builder let a caller put it in a WHERE clause. Since the
API never returns the column, the row/X-Total-Count difference is a blind
boolean oracle that recovers the secret character by character (sibling of
CVE-2025-30086, matching the guarded reg.access_secret). Add filter:"false".
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* test(scanner): regression for access_cred filter oracle (GHSA-vwvv-675v-p6rr)
Drives the real ListRegistrations/GetTotalOfRegistrations DAO path (the call
ListScannerCandidatesOfProject makes) with a non-matching access_cred filter and
asserts list/count are unchanged, i.e. the filter is dropped. Red without the
filter:"false" tag (oracle excludes the row), green with it. Positive control
keeps name filtering working.
Signed-off-by: Vadim Bauer <vb@container-registry.com>
* chore: describe the fix without advisory identifiers
Signed-off-by: Vadim Bauer <vb@container-registry.com>
(cherry picked from commit 9c160302ee3c56394957b263061adf374db4502f)
* docs(scanner): describe access_cred storage accurately
New writes are encrypted by the scanner manager; rows saved before that
remain plaintext because there is no data migration.
Signed-off-by: Vadim Bauer <vb@container-registry.com>
---------
Signed-off-by: Vadim Bauer <vb@container-registry.com>