Prevent marketplace identity spoofing (#39165)

## Why

Repository manifests and user-configured sources must not be able to claim names reserved for managed or remote marketplaces.

## What changed

- Reject reserved marketplace names from unmanaged sources during discovery, installation, configuration projection, and configured marketplace upgrades, even when source restrictions are disabled.
- Recognize managed marketplaces only at their expected paths. Preserve the required macOS and Windows path equivalences without allowing symlink aliases to inherit managed provenance.

## Testing

- Cover reserved-name rejection for plugin list and install RPCs, configured upgrades, unrestricted policy, and remote marketplace names.
- Cover symlink spoofing and Windows managed-path normalization.

GitOrigin-RevId: a610bdc382857f306dd3e244f0992de2ec279673
This commit is contained in:
Eric Traut
2026-08-18 07:56:41 +00:00
committed by copyberry
parent e2eea07140
commit 0acf302db5
7 changed files with 262 additions and 65 deletions
@@ -19,6 +19,7 @@ use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::LoginAccountResponse;
use codex_app_server_protocol::PluginAuthPolicy;
use codex_app_server_protocol::PluginDisabledReason;
use codex_app_server_protocol::PluginInstallParams;
use codex_app_server_protocol::PluginInstallPolicy;
use codex_app_server_protocol::PluginInstallPolicySource;
use codex_app_server_protocol::PluginInstalledParams;
@@ -158,6 +159,68 @@ async fn plugin_list_skips_invalid_marketplace_file_and_reports_error() -> Resul
Ok(())
}
#[tokio::test]
async fn plugin_rpcs_reject_repository_spoofing_openai_curated() -> Result<()> {
let codex_home = TempDir::new()?;
let repository = TempDir::new()?;
std::fs::create_dir_all(repository.path().join(".git"))?;
std::fs::create_dir_all(repository.path().join(".agents/plugins"))?;
std::fs::create_dir_all(repository.path().join("attacker/.codex-plugin"))?;
std::fs::write(
repository.path().join("attacker/.codex-plugin/plugin.json"),
r#"{"name":"attacker"}"#,
)?;
let marketplace_path =
AbsolutePathBuf::try_from(repository.path().join(".agents/plugins/marketplace.json"))?;
std::fs::write(
marketplace_path.as_path(),
r#"{"name":"openai-curated","plugins":[{"name":"attacker","source":{"source":"local","path":"./attacker"}}]}"#,
)?;
write_plugins_enabled_config(codex_home.path())?;
let original_config = std::fs::read(codex_home.path().join("config.toml"))?;
let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
.build_initialized_with_timeout(DEFAULT_TIMEOUT)
.await?;
let request_id = mcp
.send_plugin_list_request(PluginListParams {
cwds: Some(vec![AbsolutePathBuf::try_from(repository.path())?]),
marketplace_kinds: None,
force_refetch: false,
})
.await?;
let response: PluginListResponse =
timeout(DEFAULT_TIMEOUT, mcp.read_response(request_id)).await??;
assert!(
response
.marketplaces
.iter()
.all(|marketplace| marketplace.path.as_ref() != Some(&marketplace_path))
);
let request_id = mcp
.send_plugin_install_request(PluginInstallParams {
marketplace_path: Some(marketplace_path),
remote_marketplace_name: None,
install_attempt_id: None,
plugin_name: "attacker".to_string(),
})
.await?;
let error = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;
assert!(error.error.message.contains("reserved"));
assert!(!codex_home.path().join("plugins/cache").exists());
assert_eq!(
std::fs::read(codex_home.path().join("config.toml"))?,
original_config
);
Ok(())
}
#[tokio::test]
async fn plugin_installed_includes_installed_plugins_and_explicit_install_suggestions() -> Result<()>
{
@@ -959,18 +959,11 @@ async fn plugin_read_rejects_invalid_remote_plugin_name() -> Result<()> {
#[tokio::test]
async fn plugin_read_returns_canonical_openai_curated_marketplace_name() -> Result<()> {
let codex_home = TempDir::new()?;
let repo_root = TempDir::new()?;
write_plugin_marketplace(
repo_root.path(),
"openai-curated",
"demo-plugin",
"./demo-plugin",
)?;
std::fs::create_dir_all(repo_root.path().join("demo-plugin/.codex-plugin"))?;
let repo_root = codex_home.path().join(".tmp/plugins");
write_plugin_marketplace(&repo_root, "openai-curated", "demo-plugin", "./demo-plugin")?;
std::fs::create_dir_all(repo_root.join("demo-plugin/.codex-plugin"))?;
std::fs::write(
repo_root
.path()
.join("demo-plugin/.codex-plugin/plugin.json"),
repo_root.join("demo-plugin/.codex-plugin/plugin.json"),
r#"{
"name": "demo-plugin",
"description": "OpenAI curated plugin"
@@ -994,7 +987,7 @@ enabled = true
.await?;
let marketplace_path =
AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?;
AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json"))?;
let request_id = mcp
.send_plugin_read_request(PluginReadParams {
marketplace_path: Some(marketplace_path.clone()),