Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions agents/hermes/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1664,6 +1664,15 @@ _nemoclaw_ca_merge_warn() {
echo "[nemoclaw] WARNING: corporate proxy CA merge failed at ${1}; keeping OpenShell-only trust — external TLS through the corporate proxy may fail (#6210)" >&2
}
merge_corporate_proxy_ca() {
# Trust-anchor tampering (#8650): the image bakes this path as a root-owned
# 0444 regular file, so a symlink here is never a legitimate state. Reject it
# before any read; following it would merge attacker-chosen bytes into the
# trust bundle. This is not the recoverable "merge failed" case below, which
# safely keeps OpenShell-only trust, so it fails closed instead of warning.
if [ -L "$_NEMOCLAW_CORPORATE_CA_FILE" ]; then
echo "[nemoclaw] refusing symlinked corporate CA at ${_NEMOCLAW_CORPORATE_CA_FILE}; expected a regular file (#8650)" >&2
exit 1
fi
[ -s "$_NEMOCLAW_CORPORATE_CA_FILE" ] || return 0
_base_bundle=""
if [ -n "${SSL_CERT_FILE:-}" ] && [ -f "${SSL_CERT_FILE}" ]; then
Expand Down
9 changes: 9 additions & 0 deletions scripts/nemoclaw-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3188,6 +3188,15 @@ _nemoclaw_ca_merge_warn() {
echo "[nemoclaw] WARNING: corporate proxy CA merge failed at ${1}; keeping OpenShell-only trust — external TLS through the corporate proxy may fail (#6210)" >&2
}
merge_corporate_proxy_ca() {
# Trust-anchor tampering (#8650): the image bakes this path as a root-owned
# 0444 regular file, so a symlink here is never a legitimate state. Reject it
# before any read; following it would merge attacker-chosen bytes into the
# trust bundle. This is not the recoverable "merge failed" case below, which
# safely keeps OpenShell-only trust, so it fails closed instead of warning.
if [ -L "$_NEMOCLAW_CORPORATE_CA_FILE" ]; then
echo "[nemoclaw] refusing symlinked corporate CA at ${_NEMOCLAW_CORPORATE_CA_FILE}; expected a regular file (#8650)" >&2
exit 1
fi
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
[ -s "$_NEMOCLAW_CORPORATE_CA_FILE" ] || return 0
_base_bundle=""
if [ -n "${SSL_CERT_FILE:-}" ] && [ -f "${SSL_CERT_FILE}" ]; then
Expand Down
44 changes: 43 additions & 1 deletion test/corporate-ca-runtime-merge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// entrypoints. Exercises the actual shell blocks extracted from
// scripts/nemoclaw-start.sh and agents/hermes/start.sh, not a re-implementation.

import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { existsSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
Expand Down Expand Up @@ -39,6 +39,48 @@ function mergeBlock(scriptPath: string, endMarker: string, corpCa: string, merge
.replaceAll("/tmp/nemoclaw-ca-bundle.pem", merged);
}

const OPENCLAW_END = "# Git TLS CA bundle fix (NemoClaw#2270).";
const HERMES_END = "# OpenShell injects SSL_CERT_FILE/CURL_CA_BUNDLE for its L7 proxy CA.";

/**
* Run a merge block whose stderr is captured to a file, and return that
* diagnostic. The block exits non-zero on a rejected trust anchor, so the
* caller asserts the throw and then reads the diagnostic this leaves behind.
*/
function mergeDiagnostic(dir: string, block: string, lines: string[] = []): string {
const errFile = join(dir, "merge-stderr.txt");
expect(() =>
runShellLines(dir, [`exec 2>${JSON.stringify(errFile)}`, ...lines, block]),
).toThrow();
return readFileSync(errFile, "utf-8");
}

describe("corporate proxy CA trust-anchor rejection (#8650)", () => {
it.each([
{ agent: "OpenClaw", script: OPENCLAW_START, end: OPENCLAW_END },
{ agent: "Hermes", script: HERMES_START, end: HERMES_END },
])("rejects a symlinked corporate CA before reading it for $agent (#8650)", ({ script, end }) => {
const dir = tmpDir("nemoclaw-corp-symlink-");
const openshell = join(dir, "openshell-ca.pem");
const corp = join(dir, "corporate-ca.pem");
const merged = join(dir, "merged-ca.pem");
const planted = join(dir, "not-a-certificate");
writeFileSync(openshell, OPENSHELL_PEM);
// The baked path is a root-owned 0444 regular file in the image, so a
// symlink here is tampering. Point it at content that is not a certificate.
writeFileSync(planted, "PLANTED-NOT-A-CERTIFICATE\n");
symlinkSync(planted, corp);

const diagnostic = mergeDiagnostic(dir, mergeBlock(script, end, corp, merged), [
`export SSL_CERT_FILE=${JSON.stringify(openshell)}`,
]);

expect(diagnostic).toContain("corporate CA");
expect(diagnostic).not.toContain("PLANTED-NOT-A-CERTIFICATE");
expect(existsSync(merged)).toBe(false);
});
});

describe("corporate proxy CA runtime merge (#6210)", () => {
it("appends the corporate CA to the OpenShell bundle for OpenClaw and repoints all CA env (#6210)", () => {
const dir = tmpDir("nemoclaw-corp-merge-openclaw-");
Expand Down
Loading