Close the cheap CodeQL findings: pinned actions, token scopes, TLS identity, private temp files - #1134
Conversation
maximthomas
left a comment
There was a problem hiding this comment.
praise: Each fix lands where its CodeQL alert points, and each comes with a test.
AMSetupUtils.openConnectionno longer installs a trust-allHostnameVerifier, sogetRemoteServerInfono longer posts the admin password to a server whose certificate names a different host.AMSendMail.postMailturns on the identity check that the pinned jakarta.mail 2.0.2 leaves off by default (SocketFetcher.configureSSLSocketreadsmail.smtp.ssl.checkserveridentitywith defaultfalse).
question (blocking): Is it intended that existing smtps deployments whose certificate does not name the configured SMTP host stop sending mail after the upgrade, with no way to opt out?
openam-core/src/main/java/com/iplanet/am/util/AMSendMail.java:232-236, :226
jakarta.mail 2.0.2 leaves mail.smtp.ssl.checkserveridentity off by default, so today smtps accepts any certificate the JVM trusts. postMail builds a fresh Properties with no defaults, so after this change no system property or SMS key can turn the check off again. Some deployments set the host to an IP literal without an IP SAN, a short name, or a relay alias the certificate does not list. Those now fail with "Can't verify identity of server", which breaks password reset, lockout notification, the REST email service and HOTP/OAuth2 email OTP (HOTP login over email fails). The description says these fixes carry no behaviour risk. If the break is deliberate, a release-note entry settles it and the toggle below becomes a suggestion. If it was not considered, an opt-out that defaults to on keeps the hardening and gives operators a way back:
if (ssl) {
moduleProps.put("mail.smtp.ssl.enable", "true");
// The certificate has to be the SMTP host's, not merely one the JVM trusts.
moduleProps.put("mail.smtp.ssl.checkserveridentity", String.valueOf(SystemProperties.getAsBoolean(
"org.openidentityplatform.openam.smtp.checkServerIdentity", true)));
}Or: keep the check unconditional and add the upgrade impact to the release notes. If the key becomes an advanced server property, it also needs registering in validserverconfig, or saving it is rejected as an unknown property.
issue (non-blocking): The three private-temp-file tests pin the extracted helpers but not the call sites that use them.
openam-authentication/openam-auth-nt/src/main/java/com/sun/identity/authentication/modules/nt/NT.java:241, openam-core-rest/src/main/java/org/forgerock/openam/core/rest/docs/api/ApiDocsService.java:159, :179, openam-cassandra/openam-cassandra-embedded/src/main/java/org/openidentityplatform/openam/cassandra/embedded/Server.java:80
NTCredentialsFileTest, ApiDocsServiceTest and ServerStorageDirectoryTest call NT.createCredentialsFile(), ApiDocsService.privateTempFile(...) and Server.privateDirectory(...) directly. No test runs NT.process or ApiDocsService.getDocs/getAsciiDoc, and ServerTest.start_test asserts nothing after run(). Put any of these call sites back to File.createTempFile(...) or path.mkdirs() and all three tests stay green, including for the file that carries the Samba password. The description says each test was watched failing on the previous code; that holds for the helper bodies only. ServerTest can pin the Cassandra call site:
@Test
public void start_test() throws IdRepoException, IOException {
cassandra=new Server();
cassandra.run();
assumeTrue(FileSystems.getDefault().supportedFileAttributeViews().contains("posix"));
assertEquals(PosixFilePermissions.fromString("rwx------"),
Files.getPosixFilePermissions(Paths.get(System.getProperty("cassandra.storagedir"))));
}Pin: on a fresh runner, a path.mkdirs() revert leaves embeddedCassandra at rwxr-xr-x and this assertion fails. The imports it needs are java.io.IOException, java.nio.file.{FileSystems,Files,Paths}, java.nio.file.attribute.PosixFilePermissions, static org.junit.Assert.assertEquals and static org.junit.Assume.assumeTrue. For NT.process and ApiDocsService.getAsciiDoc, either drive the call site through a seam or say in the description that only the helper is pinned.
suggestion (non-blocking): Nothing pins the owner-only attribute that createDirectories applies to the directories it creates.
openam-cassandra/openam-cassandra-embedded/src/test/java/ServerStorageDirectoryTest.java:49, openam-cassandra/openam-cassandra-embedded/src/main/java/org/openidentityplatform/openam/cassandra/embedded/Server.java:65-67
privateDirectory always runs setPosixFilePermissions(path, ownerOnly) after createDirectories(path, asFileAttribute(ownerOnly)), and both tests read only the final permissions of the leaf. Remove the attribute and the directory is created at the umask default, then chmod'ed. Both tests stay green (measured by running both test bodies against the mutant in a scratch harness, not surefire). A missing parent gets its permissions only from the attribute and is never chmod'ed, so asserting on it pins the attribute:
@Test
public void missingParentsAreCreatedForTheOwnerOnly() throws IOException {
Path parent = base.resolve("parent");
Server.privateDirectory(parent.resolve("embeddedCassandra"));
assertEquals(EnumSet.of(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE), Files.getPosixFilePermissions(parent));
}Pin: drop asFileAttribute(ownerOnly) and parent is created rwxr-xr-x, so the case fails.
suggestion (non-blocking): Nothing pins that privateDirectory refuses a storage directory owned by another account.
openam-cassandra/openam-cassandra-embedded/src/test/java/ServerStorageDirectoryTest.java:59
The fix exists for a /tmp/embeddedCassandra that another account created first: setPosixFilePermissions on it fails with EPERM and run() stops. The existing-directory test uses a directory the test account owns, so wrapping the chmod in try { … } catch (IOException e) { } keeps both tests green (measured in a scratch harness). A root-owned directory, used from a non-root account, pins it:
@Test(expected = FileSystemException.class)
public void aDirectoryOwnedByAnotherAccountIsRefused() throws IOException {
assumeFalse("root".equals(System.getProperty("user.name")));
Server.privateDirectory(Paths.get("/"));
}Pin: with the chmod failure swallowed, no exception is thrown and the case fails. The imports it needs are java.nio.file.FileSystemException, java.nio.file.Paths and static org.junit.Assume.assumeFalse.
issue (non-blocking): ServerStorageDirectoryTest leaves an embedded-cassandra-test* directory in the shared temporary directory on every run.
openam-cassandra/openam-cassandra-embedded/src/test/java/ServerStorageDirectoryTest.java:42-46
@Before creates a new temporary directory for each test method and nothing deletes it; the second test also leaves its embeddedCassandra/data file inside. The PR's other new tests clean up after themselves (NTCredentialsFileTest:50, ApiDocsServiceTest:46).
@Rule
public TemporaryFolder tmp = new TemporaryFolder();
@Before
public void posixOnly() throws IOException {
assumeTrue(FileSystems.getDefault().supportedFileAttributeViews().contains("posix"));
base = tmp.newFolder("embedded-cassandra-test").toPath();
}suggestion (non-blocking): ServerStorageDirectoryTest does not need Cassandra, yet in CI it runs only on ubuntu × JDK 11.
openam-cassandra/openam-cassandra-embedded/src/test/java/ServerStorageDirectoryTest.java:38
The openam-cassandra parent pom skips surefire on JDK ≥ 15 (jdk-15-cassandra), on aarch64 (arm64, which includes macos-latest) and on Windows. Of the nine build-maven legs, only ubuntu-latest × 11 runs this pure filesystem test, and a local run on an arm64 Mac skips it too. The description mentions the JDK ≥ 15 skip.
Pin: add a dedicated surefire execution in openam-cassandra-embedded/pom.xml that includes only ServerStorageDirectoryTest and does not inherit the profiles' skip. Or move privateDirectory into a module that is tested on every leg.
b2cbef8 to
9b979dd
Compare
|
Addressed in 9b979dd; the branch is rebased onto current SMTP server identity (blocking). Not considered, and the break is real — so the opt-out went in, default on, as you proposed: One more case for the same list: jakarta.mail matches through Call sites of the temp-file helpers. Creation attribute of missing parents. Added Directory owned by another account. Added Leftover temporary directories. Only ubuntu × JDK 11 runs it. Recording this, not doing it here. What the test checks is POSIX All three new cases, and the |
maximthomas
left a comment
There was a problem hiding this comment.
praise: Round 1's blocker is fixed the right way. The new property is registered, documented and pinned.
org.openidentityplatform.openam.smtp.checkServerIdentitydefaults to on and is registered invalidserverconfig.properties:96, so it survives a console save andUpgradeServerDefaultsStep.shouldLeaveTheServerIdentityUncheckedWhenTheDeploymentTurnsItOffpins thefalsepath.ServerTest:59now asserts on the directoryServer.run()leaves, not only onprivateDirectory.aDirectoryOwnedByAnotherAccountIsRefusedskips whenFiles.isWritable("/"), so a build run as root never chmods/.
issue (blocking): Merged onto current master, the build-docker job has two permissions keys.
.github/workflows/build.yml:80-81
After this PR's last push, #1141 (aba00acdad, Trivy) added its own permissions: block to build-docker, after runs-on: contents: read and security-events: write. This PR adds permissions: contents: read before runs-on. The hunks do not overlap, so git merge-tree --write-tree origin/master 9b979dd is clean and GitHub offers the merge button. The merged file carries jobs.build-docker.permissions twice, at lines 80 and 83. GitHub rejects a workflow with a duplicate key ("'permissions' is already defined"), so after the merge the Build workflow would not run on master at all, and neither would deploy.yml, which is chained off it. The green checks on this PR ran on a test merge with the old base 1ceea86, so they cannot show this.
To fix it: rebase onto master and keep master's block, which already scopes the job:
build-docker:
needs: build-maven
runs-on: 'ubuntu-latest'
permissions:
contents: read
# upload the Trivy scan of the built image to code scanning
security-events: writeThe description's "build-maven / build-docker contents: read" line then needs security-events: write for build-docker.
issue (non-blocking): An empty value of the new property turns the SMTP identity check off, although the docs say "Default: true".
openam-core/src/main/java/com/iplanet/am/util/AMSendMail.java:236-237
SystemProperties.getAsBoolean(key, true) returns the default only for null and otherwise returns Boolean.parseBoolean(value). ServerPropertyValidator.validate checks a value against true,false only when value.length() > 0. An empty value saved on the Advanced tab is stored and read back as "", and parseBoolean("") is false. The same goes for a -D value such as yes, which never reaches the validator. In either case smtps quietly accepts any trusted certificate again, and nothing shows it, because mail keeps flowing. If only an explicit false turns the check off, the check fails closed:
moduleProps.put("mail.smtp.ssl.checkserveridentity", String.valueOf(
!"false".equalsIgnoreCase(SystemProperties.get(Constants.AM_SMTP_CHECK_SERVER_IDENTITY, "true").trim())));With this change, shouldLeaveTheServerIdentityUncheckedWhenTheDeploymentTurnsItOff has to stub SystemProperties.get(key, "true") instead of getAsBoolean.
issue (non-blocking): The Configuration Reference says an IP-address mail host needs --add-exports to match an IP SAN. On JDK 11 it matches without the flag.
openam-documentation/openam-doc-source/src/main/asciidoc/reference/chap-config-ref.adoc:6130
jakarta.mail 2.0.2 reaches sun.security.util.HostnameChecker by reflection. On JDK 9-15 the default --illegal-access=permit allows that call, so the IP match works with no flag. We ran jakarta.mail's own SocketFetcher.matchCert against a certificate with SAN=ip:127.0.0.1:
- JDK 11.0.25, no flag:
127.0.0.1matches,127.0.0.2does not. - JDK 17.0.11, no flag: neither matches.
The sentence is right only on JDK 16 and later.
A host given as an IP address matches an IP address subject alternative name on Java 11 as is, and on Java 16 and later only when the JVM is started with `--add-exports java.base/sun.security.util=ALL-UNNAMED`, as the OpenAM Docker image is; otherwise use a host name.
suggestion (non-blocking): ServerTest catches a revert of the run() call site only when java.io.tmpdir is clean.
openam-cassandra/openam-cassandra-embedded/src/test/java/ServerTest.java:59-60, Server.java:79-80
init() sets no cassandra.storagedir, so run() uses the fixed ${java.io.tmpdir}/embeddedCassandra, and nothing removes that directory afterwards. After one run it is left behind as rwx------. If path.mkdirs() is then put back at Server.java:80, the test still passes on that machine, because mkdirs() does not touch an existing directory. On GitHub's fresh VMs the assertion does fail. On a developer machine or a self-hosted runner it does not.
@BeforeClass
public static void init() throws IdRepoException, IOException {
// A storage directory that does not exist yet, so the assertion sees what run() creates.
System.setProperty("cassandra.storagedir",
Files.createTempDirectory("server-test").resolve("embeddedCassandra").toString());
...Pin: with this init(), the path.mkdirs() mutant fails with rwxr-xr-x on every machine, not only on a fresh one.
…entity, private temp files - Pin the third-party actions in build.yml and release.yml to commit SHAs and declare the GITHUB_TOKEN permissions each job needs. - AMSendMail asks Jakarta Mail to check the SMTP server identity on SSL. - AMSetupUtils.getRemoteServerInfo keeps the JDK host name verification: the admin password is posted over that connection. - The API docs, the NT credentials file and the embedded Cassandra storage directory are created readable by the server's own account only.
The third-party actions are pinned to commit SHAs, so nothing would update them; a github-actions Dependabot configuration does, keeping the version comment in step, one grouped pull request a week.
Add "ci"/"dependencies" labels and the grouping comment to .github/dependabot.yml, matching OpenIdentityPlatform/OpenIG#170.
…-file call sites - org.openidentityplatform.openam.smtp.checkServerIdentity (default true) turns mail.smtp.ssl.checkserveridentity off again for an SMTP host its certificate does not name; registered in validserverconfig and described in the Configuration Reference. - ServerTest checks the storage directory run() leaves behind; ServerStorageDirectoryTest pins the creation attribute on missing parents and the refusal of a directory owned by another account, and cleans up after itself.
…est on a fresh storage directory
- AMSendMail turns mail.smtp.ssl.checkserveridentity off only for an explicit
"false": the server property validator lets an empty value through, and
Boolean.parseBoolean("") would have turned the check off silently.
shouldKeepTheServerIdentityCheckForAnEmptyValue pins it.
- The Configuration Reference says an IP-address mail host matches an IP SAN
on Java 11 as is, and needs --add-exports only on Java 16 and later.
- ServerTest points cassandra.storagedir at a new directory under target, so
an embeddedCassandra an earlier run left in java.io.tmpdir cannot hide a
revert of the run() call site.
9b979dd to
587492f
Compare
|
Addressed in 587492f; the branch is rebased onto current Duplicate Empty value turns the check off. Confirmed: IP SAN on Java 11. Confirmed with the same
The Configuration Reference takes your sentence; the Upgrade impact note in the description is corrected the same way.
|
maximthomas
left a comment
There was a problem hiding this comment.
praise: The rebase resolves the workflow conflict the right way round, and the SMTP opt-out now fails closed.
.github/workflows/build.yml:81-84:build-dockerkeeps master's singlepermissionsblock (contents: read+security-events: write); merged onto currentmaster(394a779) the workflow still has one block per job.openam-core/src/main/java/com/iplanet/am/util/AMSendMail.java:237-238keeps the identity check for an empty value; onlyfalseturns it off.ServerTest.initpointscassandra.storagedirat a directory undertargetthat does not exist yet, so a staleembeddedCassandrainjava.io.tmpdircan no longer mask a revert ofrun().
The mechanical part of the CodeQL medium triage: findings whose fix is a few lines, grouped in one PR. Two of them change what an existing deployment sees — the SMTP and the setup host name checks below; each says so under Upgrade impact.
Workflows (
actions/unpinned-tag×11,actions/missing-workflow-permissions×5)docker/metadata-action,docker/setup-qemu-action,docker/setup-buildx-action,docker/login-action,docker/build-push-actionandsoftprops/action-gh-releaseare pinned to the commit their major tag resolves to today, with the precise release in a comment (v6.2.0, v4.4.0, v4.4.1, v4.6.0, v7.4.0, v3.0.3).actions/*stay on tags: first-party, and not what the query flags..github/dependabot.yml(new) enables thegithub-actionsecosystem, weekly and grouped into one pull request: a SHA pin is frozen by definition, so without it the actions would never move again. Dependabot updates the SHA and the version comment together, and the grouped PR carries theci/dependencieslabels, matching Harden GitHub Actions workflows: token permissions, SHA pinning, latest actions OpenIG#170.GITHUB_TOKENscopes it uses:build-mavencontents: read;build-dockerkeeps the block Add Trivy vulnerability scanning for the Docker image #1141 gave it onmaster,contents: read+security-events: write(the image goes to a local registry; the Trivy scan is uploaded to code scanning);deploy-mavencontents: write(docs are pushed to the wiki withgithub.token);release-mavencontents: write(release:preparepushes the tag, the GitHub release is created, the wiki is pushed);release-dockercontents: read+packages: write(GHCR login withGITHUB_TOKEN). The Docker Hub and doc-repo pushes use their own secrets and need nothing from the token.TLS identity (
java/insecure-smtp-ssl×2,java/unsafe-hostname-verification1 of 3)AMSendMail.postMail(…, ssl=true)setsmail.smtp.ssl.checkserveridentity: the SMTP server's certificate has to be the host's, not merely one the JVM trusts. It is on by default and can be turned off with the new advanced server propertyorg.openidentityplatform.openam.smtp.checkServerIdentity=false, registered invalidserverconfig.properties(otherwise saving it is rejected, and the upgrade step would drop it from the server defaults) and described in the Configuration Reference. Only an explicitfalseturns the check off: the validator lets an empty value through, and an empty or unrecognised value keeps the check rather than parse asfalse. Pinned byshouldCheckTheServerIdentityOnAnSslConnection(default),shouldLeaveTheServerIdentityUncheckedWhenTheDeploymentTurnsItOffandshouldKeepTheServerIdentityCheckForAnEmptyValue.Can't verify identity of server, which stops password reset, lockout notification, the Email Service and the HOTP / OAuth2 email one-time passwords. A host given as an IP address matches an IP SAN on Java 11 as is, and on Java 16 and later only when the JVM runs with--add-exports java.base/sun.security.util=ALL-UNNAMED(the Docker image does): jakarta.mail reachesHostnameCheckerby reflection, which Java 11's default--illegal-access=permitallows, and without it falls back to comparing DNS names and the CN. The fix is a host name the certificate carries, or the property above.AMSetupUtils.openConnectionno longer installs a trust-allHostnameVerifier.getRemoteServerInfoposts the admin password to the remote server over that connection, so a certificate that is valid for some other host must not do.openConnectionis package-private forshouldKeepTheDefaultHostNameVerifierForARemoteServerOverHttps.ClusterStateService(a site health probe whose answer is a boolean; strict verification would break clusters whose certificates do not name the internal host names) andSoapSTSConsumer.handleSTSServerCertCNDNSMismatch(a test helper whose javadoc already says it must not be relied upon in production).Private temporary files (
java/local-temp-file-or-directory-information-disclosure×4)File.createTempFilecreates the file with the process umask —rw-r--r--on a typical server — in the shared temporary directory.java.nio.file.Files.createTempFilecreates itrw-------.ApiDocsService: the generated.asciidocand.htmlgo throughprivateTempFile(owner-only, deleted on exit).NT: the file that carries the user name and password to the Samba helper goes throughcreateCredentialsFile; its name no longer embeds the user name (which also failed for names shorter than three characters, the prefix minimum).Server: the storage directory keeps its fixed default underjava.io.tmpdir— a random name would not survive a restart of the development store — but is created, missing parents included, and if it already exists set, torwx------where the file system is POSIX; elsewhere it is created as before. A directory another account created first cannot be set, and the store refuses to start in it.What the tests pin:
NTCredentialsFileTestandApiDocsServiceTestpin the helpersNT.createCredentialsFileandApiDocsService.privateTempFileonly, not their call sites inNT.processandApiDocsService.getDocs/getAsciiDoc: the former runs the Samba helper, the latter needs the router and Asciidoctor. Both call sites are one line each, shown in the diff.ServerTestchecks the storage directoryrun()leaves behind, and pointscassandra.storagedirat a directory undertargetthat does not exist yet, so a directory an earlier run left injava.io.tmpdircannot mask a revert.ServerStorageDirectoryTestpins the new directory, missing parents (which get their permissions from the creation attribute alone), an existing directory (kept data, permissions tightened) and a directory owned by another account (refused; skipped where the test account could write to/).[OWNER_READ, OWNER_WRITE, GROUP_READ, OTHERS_READ](files) and0755/ inherited (directory);ServerTestwithpath.mkdirs()back inrun(), also with a staleembeddedCassandraleft injava.io.tmpdir; the empty-value case withSystemProperties.getAsBooleanback inAMSendMail; the parent case without the creation attribute; the other-account case with the permission failure swallowed.Verification
Rebased onto
masterataba00acdad(#1141); thebuild-dockerpermissions are resolved in the first commit, and the mergedbuild.ymlcarries onepermissionsblock per job. On that base, under JDK 11 on x86_64:openam-cassandra-embedded5 andAMSendMailTest32. The Cassandra module's tests are skipped on JDK ≥ 15, on aarch64 and on Windows by the parent pom's profiles, so in CI they run on the ubuntu-latest × 11 leg only — includingServerTest, which starts the embedded Cassandra through the changedrun(). On the earlier base1ceea86, under JDK 26:openam-shared1237,openam-core2134,openam-core-rest441,openam-auth-nt7. 0 failures.NTCredentialsFileTestis a separate plain class becauseNTTestruns under PowerMock's class loader, which cannot instrumentsun.nio.fson JDK 26.The CodeQL bot's SSRF flag on
AMSetupUtils.openConnection(alert #511) is triaged and dismissed aswon't fix— a re-fingerprint of #145.