Skip to content

Security audit: MEDIUM fixes — credential binding, transport, validation, deployAtEnd - #698

Draft
gnodet wants to merge 8 commits into
masterfrom
security/audit-medium-fixes
Draft

Security audit: MEDIUM fixes — credential binding, transport, validation, deployAtEnd#698
gnodet wants to merge 8 commits into
masterfrom
security/audit-medium-fixes

Conversation

@gnodet

@gnodet gnodet commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Summary

Applies the 6 MEDIUM-severity findings from the Glasswing static security audit (scan-maven-deploy-plugin-20260811) — all confirmed TP with 5/5 adversarial-panel unanimity and zero false positives.

Findings addressed

Finding Severity Title Patch
f001 MEDIUM Unvalidated repository-id→URL binding routes arbitrary settings.xml credentials to arbitrary URLs bug_01
f002 MEDIUM No transport-scheme validation on any deployment path — cleartext http/ftp accepted silently bug_02
f003 MEDIUM deploy-file layout validation incomplete: classifier/types/packaging unvalidated; dot-only coordinates pass bug_03
f004 MEDIUM deployAtEnd never transitions to DEPLOYED — re-triggered batch re-publishes the whole reactor bug_04
f005 MEDIUM Unsynchronized check-then-act on the batch trigger — -T builds can double-fire the batch bug_05
f006 MEDIUM deployAtEnd documented fail-closed atomicity is false (three legs) bug_06

Changes

Cluster 1 — Credential and transport security (f001, f002):

  • validateCredentialBinding: provenance-aware guard that refuses POM-sourced alt-repo values binding a credentialed id to an unknown URL; CLI-typed values proceed with WARN. New knob: maven.deploy.allowCredentialReuse (user-property only, non-POM-bindable)
  • validateTransportSecurity: refuses http:// and ftp:// deploy URLs (loopback exempt, file:// untouched). New knob: maven.deploy.allowInsecureUrl
  • Shipped http doc examples fixed inline

Cluster 2 — Layout validation (f003):

  • Shared validators hoisted to AbstractDeployMojo: dot-only segments rejected for id/version/classifier/type; classifier/classifiers/types/packaging (previously never validated) now allowlisted in both mojos

Cluster 3 — deployAtEnd state machine (f004, f005, f006):

  • deployAllAtOnce marks each batched project State.DEPLOYED after whole-batch success; re-entry is a logged no-op
  • DEPLOY_AT_END_LOCK monitor serializes the mark-then-check-then-fire sequence across -T reactor threads
  • False atomicity javadoc replaced with accurate non-atomicity enumeration; "(experimental)" removed from the default-on path; partial failure logs ERROR naming repositories already published

Breaking changes

  • http:// and ftp:// deploy URLs now refused by default (override: -Dmaven.deploy.allowInsecureUrl=true)
  • POM-sourced alt-repo values binding credentials to unknown URLs now refused (override: -Dmaven.deploy.allowCredentialReuse=true)
  • skip vocabulary is now fail-closed: unrecognized values (typos, wrong case) fail the build instead of silently deploying

Test plan

  • All 36 existing tests pass
  • 3 new provenance-aware credential tests pass (bug_01)
  • 3 new transport security tests pass (bug_02)
  • New deployAtEnd re-deploy test passes (bug_04)
  • Build verified locally with Maven 4 rc-5

Audit source: scan-maven-deploy-plugin-20260811.zip — 5-researcher static audit, 80/80 adversarial-panel votes, 0 FP

🤖 Generated with Claude Code

gnodet and others added 7 commits August 31, 2026 13:46
…eCredentialBinding guard

Security audit finding f001 (MEDIUM).
Keeps v1's structure: AbstractDeployMojo.validateCredentialBinding guard, known-URL collection (settings mirrors + profile repositories/pluginRepositories, plus distributionManagement via DeployMojo's getKnownRepositoryUrls override), trailing-slash URL normalization, and the user-property-only -Dmaven.deploy.allowCredentialReuse=true knob (session.getUserProperties() only, not POM-settable). v2 adds provenance semantics (fleet precedent: checkstyle bug_04 / clean bug_08): validateCredentialBinding(id, url, fromUserProperty). Mismatch against non-empty known URLs is refused regardless of provenance (unchanged from v1). Empty-record case no longer passes silently: POM-sourced (DeployMojo computes isFromUserProperty by requiring the -D user property to be present AND equal to the value in use, since explicit POM <configuration> beats -D in Maven precedence; the alt selection chain records provenance per parameter name) -> REFUSE naming id and URL, knob-overridable; CLI-sourced -> WARN (id has stored credentials, no known URL for it, credentials will be sent to <url>). DeployFileMojo keeps the 2-arg call, which delegates with fromUserProperty=true (deploy-file is CLI-driven by nature). Javadoc on getKnownRepositoryUrls documents dm-sourced known URLs as advisory in the malicious-POM model; parameter javadoc for altDeploymentRepository and repositoryId documents the provenance-dependent behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ransportSecurity(id,url)

Security audit finding f002 (MEDIUM).
Single choke point: AbstractDeployMojo.validateTransportSecurity(id,url) refuses http:// and ftp:// deployment URLs by default, called from createDeploymentArtifactRepository (covers the alt-repo route in DeployMojo and the deploy-file url) and explicitly on both distributionManagement branches (which bypass that factory via session.createRemoteRepository(Repository)). Loopback hosts (localhost, 127.0.0.0/8, ::1) are exempt so local mock/IT repositories keep working; opt-out is -Dmaven.deploy.allowInsecureUrl=true (user property, non-pom-bindable), which downgrades the refusal to a WARN. Unknown/other schemes (https, file, scp, sftp, scm:, dav:) are untouched. Docs leg: deploy-http.md example switched to https with an explicit prefer-HTTPS + refusal note; deploying-with-classifiers.md.vm http:// examples switched to https. (Wagon-era deploy-ftp.md/deploy-ssh-external.md rewrites are in bug_13.)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…alidators hoisted to Abs

Security audit finding f003 (MEDIUM).
Both legs of the root cause closed with shared validators hoisted to AbstractDeployMojo: isValidId now rejects any empty dot-separated segment (kills '.', '..', '.a', 'a.', 'a..b' while keeping the [a-zA-Z0-9._-] allowlist); isValidVersion keeps the separator blocklist and additionally rejects empty, whitespace/ISO-control characters, and all-dots values; new isValidClassifier (same allowlist, not all-dots, absent/empty allowed) and isValidTypeOrExtension (non-empty variant) cover the components the old gate never saw. DeployFileMojo now validates packaging and classifier at the :272 gate (after processModel, so jar-embedded-POM-supplied packaging is covered too) and every classifiers/types list entry inside the side-artifact loop (empty entries rejected). DeployMojo (second mojo per the finding brief) validates project g/a/v and every deployable's classifier in createDeployerRequest before building the request.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…on: (1) deployAllAtOnce

Security audit finding f004 (MEDIUM).
Two-sided state fix, per the triage recommendation: (1) deployAllAtOnce collects the batched projects while flattening and, after all requests deploy successfully, marks each one State.DEPLOYED (mark-after-batch-deploy per the design guidance - a failed batch stays TO_BE_DEPLOYED so a genuine retry of a failed build is not suppressed); (2) execute() treats DEPLOYED as terminal: re-entering the goal for an already-deployed project logs and returns, closing both triggers (double-bound executions and the direct deploy:deploy O(N^2) walk).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…, a constant lock object

Security audit finding f005 (MEDIUM).
A single class-level monitor (DEPLOY_AT_END_LOCK, a constant lock object - not mutable static state; batch state stays in per-project session plugin contexts) now serializes every state read/write and the batch trigger: the terminal-state re-entry check, the SKIPPED/DEPLOYED/TO_BE_DEPLOYED marks, and the allProjectsMarked()+deployAllAtOnce() check-then-act. The lock is deliberately held across deployAllAtOnce(): the losing thread waits, then observes the DEPLOYED states written by the winner (bug_04) and no-ops. Request construction (createDeployerRequest) stays outside the lock to keep contention minimal.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…, per the finding brief.

Security audit finding f006 (MEDIUM).
Verified-true doc fix plus visibility escalation, per the finding brief. The deployAtEnd javadoc now states the real semantics: batch fires when the last deploy-bound project reaches its deploy phase (trailing execution-less modules can still fail afterward), multi-repo/mixed-retry requests deploy sequentially with no rollback, and deployAtEnd=false modules cannot be recalled; the false 'none of the reactor projects is deployed' sentence and the incoherent '(experimental)' marker (on a default-on publish path) are removed. Partial-failure visibility: deployAllAtOnce now deploys the grouped requests in an explicit loop and, on failure after at least one group succeeded, logs an ERROR naming how many groups and which repository ids were already published ('remain published: there is no rollback') before rethrowing, so the failure is no longer attributed only to the wrong module.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ility

The deployAtEndBatchIsNotRedeployedOnReentry test needs a properly mocked
MojoExecution with plugin model, because the upstream cache optimization
(PROJECTS_WITH_DEPLOY_KEY) calls mojoExecution.getPlugin().getModel().getKey()
in hasDeployExecution().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The classifier validator from f003 wrongly rejected empty classifier
entries in the classifiers CSV (e.g. "classifiers=,src,"). Empty means
"no classifier" and is valid Maven convention used by the
3rd-party-{jar,pom}-with-extras ITs. isValidClassifier("") already
returns true; the redundant isEmpty() pre-check was the bug.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@slawekjaranowski

Copy link
Copy Markdown
Member

@gnodet I would like to extract fixes for deployAtEnd state machine in separate PR
I think we should do the same in install plugin, both can have simillary implementation

for install plugin we have:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants