mirror of
https://github.com/openai/codex.git
synced 2026-10-24 18:18:41 +00:00
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:
@@ -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()),
|
||||
|
||||
Reference in New Issue
Block a user