From d6f6b4a59e6f6a527adfcb692153e146009472ea Mon Sep 17 00:00:00 2001 From: Vladyslav Nikonov Date: Tue, 11 Aug 2026 17:38:18 +0300 Subject: [PATCH 1/9] fix(now-policy-api): allow '/' and ':' in PackageIdentifier PackageIdentifier validation rejected '/' and ':' during deserialization, breaking scoped npm/Bun packages (@scope/package), npm aliases (alias:@scope/package@^7.20.0) and vcpkg triplets (curl:x64-windows) over the wire even though the package broker ecosystem advertises support for them. Relax the grammar to allow '/' and ':' while still rejecting genuinely dangerous or ambiguous input: control characters (now including NUL and DEL), backslash, double quote, '<', '>', '|', and the wildcard characters '*' and '?' (policy-side identifier matching is wildcard-based). The schemars regex, parse(), and the generated OpenAPI document are kept in sync, and wire-level (de)serialization tests cover the new grammar. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../openapi/now-policy-api.yaml | 7 +- policies/rust/now-policy-api/src/lib.rs | 144 +++++++++++++++++- 2 files changed, 145 insertions(+), 6 deletions(-) diff --git a/policies/rust/now-policy-api/openapi/now-policy-api.yaml b/policies/rust/now-policy-api/openapi/now-policy-api.yaml index 80eff14..61d1164 100644 --- a/policies/rust/now-policy-api/openapi/now-policy-api.yaml +++ b/policies/rust/now-policy-api/openapi/now-policy-api.yaml @@ -746,11 +746,14 @@ components: format: date-time additionalProperties: false PackageIdentifier: - description: Package identifier string. + description: |- + Package identifier string. + + Allows `/` and `:` so that scoped npm/Bun packages (`@scope/package`), npm aliases (`alias:@scope/package@^1.0.0`) and vcpkg triplets (`curl:x64-windows`) are accepted. Rejects control characters, `\`, `"`, `<`, `>`, `|`, and the wildcard characters `*` and `?` (policy-side package identifier matching is wildcard-based, so wildcards in request identifiers would be ambiguous). type: string maxLength: 256 minLength: 1 - pattern: ^[^\\\/:*?"<>|\x01-\x1f]+$ + pattern: ^[^\\*?"<>|\x00-\x1f\x7f]+$ PackageRequest: description: Canonical request sent by a package broker client to the elevated broker. type: object diff --git a/policies/rust/now-policy-api/src/lib.rs b/policies/rust/now-policy-api/src/lib.rs index f194863..999c3bd 100644 --- a/policies/rust/now-policy-api/src/lib.rs +++ b/policies/rust/now-policy-api/src/lib.rs @@ -398,6 +398,12 @@ impl From<&str> for RuleId { } /// Package identifier string. +/// +/// Allows `/` and `:` so that scoped npm/Bun packages (`@scope/package`), npm aliases +/// (`alias:@scope/package@^1.0.0`) and vcpkg triplets (`curl:x64-windows`) are accepted. +/// Rejects control characters, `\`, `"`, `<`, `>`, `|`, and the wildcard characters +/// `*` and `?` (policy-side package identifier matching is wildcard-based, so wildcards +/// in request identifiers would be ambiguous). #[derive( Debug, Clone, @@ -414,7 +420,7 @@ impl From<&str> for RuleId { #[deref(forward)] #[display("{_0}")] pub struct PackageIdentifier( - #[schemars(length(min = 1, max = 256), regex(pattern = r#"^[^\\\/:*?"<>|\x01-\x1f]+$"#))] pub String, + #[schemars(length(min = 1, max = 256), regex(pattern = r#"^[^\\*?"<>|\x00-\x1f\x7f]+$"#))] pub String, ); impl PackageIdentifier { @@ -435,15 +441,14 @@ impl PackageIdentifier { if s.bytes().any(|b| { b == b'\\' - || b == b'/' - || b == b':' || b == b'*' || b == b'?' || b == b'"' || b == b'<' || b == b'>' || b == b'|' - || (0x01..=0x1f).contains(&b) + || b <= 0x1f + || b == 0x7f }) { return Err(ModelValidationError::Invalid { type_name: "PackageIdentifier", @@ -542,3 +547,134 @@ impl<'de> Deserialize<'de> for CommandString { Self::parse(&s).map_err(serde::de::Error::custom) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn package_identifier_accepts_manager_specific_identifiers() { + let valid = [ + // WinGet. + "Microsoft.VisualStudioCode", + // Scoped npm/Bun packages. + "@scope/package", + "@babel/core", + // npm aliases. + "babel-core-legacy:@babel/core@^7.20.0", + "my-alias:lodash@~4.17.21", + // vcpkg triplets and features. + "curl:x64-windows", + "curl[ssl]:x64-windows", + // Other managers (pip extras, chocolatey, scoop buckets, dotnet, cargo). + "requests[socks]", + "git.install", + "extras/vscode", + "dotnet-ef", + "serde_json", + ]; + + for id in valid { + PackageIdentifier::parse(id).unwrap_or_else(|e| panic!("{id:?} should be valid: {e}")); + + let json = serde_json::to_string(id).expect("string should serialize to JSON"); + let deserialized: PackageIdentifier = + serde_json::from_str(&json).unwrap_or_else(|e| panic!("{id:?} should deserialize: {e}")); + assert_eq!(deserialized.0, id); + } + } + + #[test] + fn package_identifier_rejects_forbidden_input() { + let invalid = [ + "", + "foo\\bar", + "foo*", + "foo?", + "foo\"bar", + "foobar", + "foo|bar", + "foo\r\nbar", + "foo\tbar", + "foo\u{0}bar", + "foo\u{7f}bar", + &"a".repeat(257), + ]; + + for id in invalid { + PackageIdentifier::parse(id).expect_err(&format!("{id:?} should be rejected")); + + let json = serde_json::to_string(id).expect("string should serialize to JSON"); + serde_json::from_str::(&json) + .expect_err(&format!("{id:?} should be rejected during deserialization")); + } + } + + #[test] + fn package_request_deserializes_scoped_and_aliased_identifiers() { + let identifiers = [ + "@scope/package", + "babel-core-legacy:@babel/core@^7.20.0", + "curl:x64-windows", + ]; + + for id in identifiers { + let json = serde_json::json!({ + "RequestKind": "PackageRequest", + "RequestVersion": "1.0", + "RequestId": "req-scoped-install", + "CreatedAt": "2026-05-05T12:00:00Z", + "Operation": "Install", + "Manager": "Npm", + "Source": { "Name": "npm" }, + "Package": { "Id": id }, + "Options": { + "Interactive": false, + "SkipHashCheck": false, + "PreRelease": false + }, + "Client": { + "RequestedElevation": "Elevated", + "EffectiveUser": "CONTOSO\\alice", + "ClientVersion": "3.2.0", + "Transport": "HttpNamedPipe", + "ClientExecutablePath": "C:\\Program Files\\Devolutions\\NOW\\now-client.exe" + } + }); + + let request: PackageRequest = serde_json::from_value(json) + .unwrap_or_else(|e| panic!("PackageRequest with id {id:?} should deserialize: {e}")); + assert_eq!(request.package.id.0, id); + } + } + + #[test] + fn package_request_rejects_invalid_identifier() { + let json = serde_json::json!({ + "RequestKind": "PackageRequest", + "RequestVersion": "1.0", + "RequestId": "req-invalid-id", + "CreatedAt": "2026-05-05T12:00:00Z", + "Operation": "Install", + "Manager": "Winget", + "Source": { "Name": "winget" }, + "Package": { "Id": "evil|package\r\n" }, + "Options": { + "Interactive": false, + "SkipHashCheck": false, + "PreRelease": false + }, + "Client": { + "RequestedElevation": "Elevated", + "EffectiveUser": "CONTOSO\\alice", + "ClientVersion": "3.2.0", + "Transport": "HttpNamedPipe", + "ClientExecutablePath": "C:\\Program Files\\Devolutions\\NOW\\now-client.exe" + } + }); + + serde_json::from_value::(json) + .expect_err("PackageRequest with forbidden identifier characters should be rejected"); + } +} From 58ccf855ff4b082e39709e749fcd539aec687213 Mon Sep 17 00:00:00 2001 From: Vladyslav Nikonov Date: Thu, 13 Aug 2026 13:20:31 +0300 Subject: [PATCH 2/9] fix(now-policy-api): validate PackageIdentifier with an explicit allowlist Replace the character denylist with an explicit allowlist derived from the identifiers actually used by supported package managers: ASCII alphanumerics plus '. - _ + @ / : ^ ~ = [ ] ,'. This keeps scoped npm/Bun packages, npm aliases, vcpkg triplets/features, homebrew/scoop tap paths, and pip extras/pins working while rejecting whitespace, shell metacharacters, wildcards, control characters, and non-ASCII input. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../openapi/now-policy-api.yaml | 16 ++++- policies/rust/now-policy-api/src/lib.rs | 71 ++++++++++++++----- 2 files changed, 66 insertions(+), 21 deletions(-) diff --git a/policies/rust/now-policy-api/openapi/now-policy-api.yaml b/policies/rust/now-policy-api/openapi/now-policy-api.yaml index 61d1164..8a30df5 100644 --- a/policies/rust/now-policy-api/openapi/now-policy-api.yaml +++ b/policies/rust/now-policy-api/openapi/now-policy-api.yaml @@ -749,11 +749,23 @@ components: description: |- Package identifier string. - Allows `/` and `:` so that scoped npm/Bun packages (`@scope/package`), npm aliases (`alias:@scope/package@^1.0.0`) and vcpkg triplets (`curl:x64-windows`) are accepted. Rejects control characters, `\`, `"`, `<`, `>`, `|`, and the wildcard characters `*` and `?` (policy-side package identifier matching is wildcard-based, so wildcards in request identifiers would be ambiguous). + Validated against an explicit allowlist of characters actually used by the supported package managers: ASCII alphanumerics plus `. - _ + @ / : ^ ~ = [ ] ,`. + + - `.`, `-`, `_`, `+`: winget (`Notepad++.Notepad++`), chocolatey, pip, cargo, dotnet, apt/dnf/pacman (`g++`, `libstdc++6`), PowerShell modules; + + - `@`, `/`: scoped npm/Bun packages (`@scope/package`), homebrew and scoop `tap/formula` paths, versioned formulas (`python@3.11`); + + - `:`: npm aliases (`alias:@scope/package@^1.0.0`), vcpkg triplets (`curl:x64-windows`); + + - `^`, `~`, `=`: version pins/ranges inside npm aliases and pip specifiers; + + - `[`, `]`, `,`: vcpkg features (`curl[ssl,http2]:x64-windows`), pip extras (`requests[socks]`). + + Everything else — including whitespace, control characters, wildcards (`*`, `?`; policy-side package identifier matching is wildcard-based, so wildcards in request identifiers would be ambiguous), shell metacharacters, and non-ASCII — is rejected. type: string maxLength: 256 minLength: 1 - pattern: ^[^\\*?"<>|\x00-\x1f\x7f]+$ + pattern: ^[A-Za-z0-9._+@/:^~=\[\],-]+$ PackageRequest: description: Canonical request sent by a package broker client to the elevated broker. type: object diff --git a/policies/rust/now-policy-api/src/lib.rs b/policies/rust/now-policy-api/src/lib.rs index 999c3bd..6af362d 100644 --- a/policies/rust/now-policy-api/src/lib.rs +++ b/policies/rust/now-policy-api/src/lib.rs @@ -399,11 +399,27 @@ impl From<&str> for RuleId { /// Package identifier string. /// -/// Allows `/` and `:` so that scoped npm/Bun packages (`@scope/package`), npm aliases -/// (`alias:@scope/package@^1.0.0`) and vcpkg triplets (`curl:x64-windows`) are accepted. -/// Rejects control characters, `\`, `"`, `<`, `>`, `|`, and the wildcard characters -/// `*` and `?` (policy-side package identifier matching is wildcard-based, so wildcards -/// in request identifiers would be ambiguous). +/// Validated against an explicit allowlist of characters actually used by the +/// supported package managers: ASCII alphanumerics plus `. - _ + @ / : ^ ~ = [ ] ,`. +/// +/// - `.`, `-`, `_`, `+`: winget (`Notepad++.Notepad++`), chocolatey, pip, cargo, +/// dotnet, apt/dnf/pacman (`g++`, `libstdc++6`), PowerShell modules; +/// +/// - `@`, `/`: scoped npm/Bun packages (`@scope/package`), homebrew and scoop +/// `tap/formula` paths, versioned formulas (`python@3.11`); +/// +/// - `:`: npm aliases (`alias:@scope/package@^1.0.0`), vcpkg triplets +/// (`curl:x64-windows`); +/// +/// - `^`, `~`, `=`: version pins/ranges inside npm aliases and pip specifiers; +/// +/// - `[`, `]`, `,`: vcpkg features (`curl[ssl,http2]:x64-windows`), pip extras +/// (`requests[socks]`). +/// +/// Everything else — including whitespace, control characters, wildcards +/// (`*`, `?`; policy-side package identifier matching is wildcard-based, so +/// wildcards in request identifiers would be ambiguous), shell metacharacters, +/// and non-ASCII — is rejected. #[derive( Debug, Clone, @@ -420,7 +436,7 @@ impl From<&str> for RuleId { #[deref(forward)] #[display("{_0}")] pub struct PackageIdentifier( - #[schemars(length(min = 1, max = 256), regex(pattern = r#"^[^\\*?"<>|\x00-\x1f\x7f]+$"#))] pub String, + #[schemars(length(min = 1, max = 256), regex(pattern = r"^[A-Za-z0-9._+@/:^~=\[\],-]+$"))] pub String, ); impl PackageIdentifier { @@ -439,20 +455,16 @@ impl PackageIdentifier { }); } - if s.bytes().any(|b| { - b == b'\\' - || b == b'*' - || b == b'?' - || b == b'"' - || b == b'<' - || b == b'>' - || b == b'|' - || b <= 0x1f - || b == 0x7f + if !s.bytes().all(|b| { + b.is_ascii_alphanumeric() + || matches!( + b, + b'.' | b'-' | b'_' | b'+' | b'@' | b'/' | b':' | b'^' | b'~' | b'=' | b'[' | b']' | b',' + ) }) { return Err(ModelValidationError::Invalid { type_name: "PackageIdentifier", - reason: "contains forbidden characters".to_owned(), + reason: "must contain only ASCII alphanumerics or '. - _ + @ / : ^ ~ = [ ] ,'".to_owned(), }); } @@ -557,21 +569,29 @@ mod tests { let valid = [ // WinGet. "Microsoft.VisualStudioCode", + "Notepad++.Notepad++", // Scoped npm/Bun packages. "@scope/package", "@babel/core", // npm aliases. "babel-core-legacy:@babel/core@^7.20.0", "my-alias:lodash@~4.17.21", + "pinned:react@=18.2.0", // vcpkg triplets and features. "curl:x64-windows", "curl[ssl]:x64-windows", - // Other managers (pip extras, chocolatey, scoop buckets, dotnet, cargo). + "curl[ssl,http2]:x64-windows", + // Homebrew/scoop tap paths and versioned formulas. + "extras/vscode", + "homebrew/core/python@3.11", + // pip extras and version pins. "requests[socks]", + "requests==2.32.0", + // Chocolatey, dotnet, cargo, apt-style names. "git.install", - "extras/vscode", "dotnet-ef", "serde_json", + "g++", ]; for id in valid { @@ -597,8 +617,20 @@ mod tests { "foo|bar", "foo\r\nbar", "foo\tbar", + "foo bar", "foo\u{0}bar", "foo\u{7f}bar", + "foo!bar", + "foo#bar", + "foo$bar", + "foo%bar", + "foo&bar", + "foo'bar", + "foo(bar)", + "foo;bar", + "foo{bar}", + "foo`bar", + "caf\u{e9}", &"a".repeat(257), ]; @@ -617,6 +649,7 @@ mod tests { "@scope/package", "babel-core-legacy:@babel/core@^7.20.0", "curl:x64-windows", + "curl[ssl]:x64-windows", ]; for id in identifiers { From fecf1b9e6ebaa6faa290ec7f6992f4513ae4b149 Mon Sep 17 00:00:00 2001 From: Vladyslav Nikonov Date: Thu, 13 Aug 2026 13:26:35 +0300 Subject: [PATCH 3/9] fix(now-policy-api): use a typos-friendly non-ASCII rejection test case Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- policies/rust/now-policy-api/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/policies/rust/now-policy-api/src/lib.rs b/policies/rust/now-policy-api/src/lib.rs index 6af362d..aec68fa 100644 --- a/policies/rust/now-policy-api/src/lib.rs +++ b/policies/rust/now-policy-api/src/lib.rs @@ -630,7 +630,7 @@ mod tests { "foo;bar", "foo{bar}", "foo`bar", - "caf\u{e9}", + "gr\u{fc}n", &"a".repeat(257), ]; From aaa15b1f73ec965ed62ac7ad3f827e2c35c3ac6a Mon Sep 17 00:00:00 2001 From: Vladyslav Nikonov Date: Thu, 13 Aug 2026 13:34:17 +0300 Subject: [PATCH 4/9] fix(now-policy-api): extend PackageIdentifier allowlist with '< > ? * |' Per review feedback, additionally allow version range operators and wildcards for future-proofing: '<' and '>' (npm/pip range specifiers), '|' ('||' alternation in npm ranges), and the wildcards '*' and '?'. Whitespace, control characters, double quote, backslash, shell metacharacters, and non-ASCII input remain rejected. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../openapi/now-policy-api.yaml | 12 +++-- policies/rust/now-policy-api/src/lib.rs | 54 +++++++++++++------ 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/policies/rust/now-policy-api/openapi/now-policy-api.yaml b/policies/rust/now-policy-api/openapi/now-policy-api.yaml index 8a30df5..32f5845 100644 --- a/policies/rust/now-policy-api/openapi/now-policy-api.yaml +++ b/policies/rust/now-policy-api/openapi/now-policy-api.yaml @@ -749,7 +749,7 @@ components: description: |- Package identifier string. - Validated against an explicit allowlist of characters actually used by the supported package managers: ASCII alphanumerics plus `. - _ + @ / : ^ ~ = [ ] ,`. + Validated against an explicit allowlist of characters: ASCII alphanumerics plus `. - _ + @ / : ^ ~ = [ ] , < > ? * |`. - `.`, `-`, `_`, `+`: winget (`Notepad++.Notepad++`), chocolatey, pip, cargo, dotnet, apt/dnf/pacman (`g++`, `libstdc++6`), PowerShell modules; @@ -757,15 +757,17 @@ components: - `:`: npm aliases (`alias:@scope/package@^1.0.0`), vcpkg triplets (`curl:x64-windows`); - - `^`, `~`, `=`: version pins/ranges inside npm aliases and pip specifiers; + - `^`, `~`, `=`, `<`, `>`, `|`: version pins/ranges in npm aliases and pip specifiers (`>=7 <8` without the space, `||` alternation); - - `[`, `]`, `,`: vcpkg features (`curl[ssl,http2]:x64-windows`), pip extras (`requests[socks]`). + - `[`, `]`, `,`: vcpkg features (`curl[ssl,http2]:x64-windows`), pip extras (`requests[socks]`); - Everything else — including whitespace, control characters, wildcards (`*`, `?`; policy-side package identifier matching is wildcard-based, so wildcards in request identifiers would be ambiguous), shell metacharacters, and non-ASCII — is rejected. + - `*`, `?`: wildcards. Note: policy-side package identifier matching is wildcard-based, so wildcards in request identifiers may behave surprisingly when matched against policy rules. + + Everything else — whitespace, control characters, `"`, `\`, shell metacharacters, and non-ASCII — is rejected. type: string maxLength: 256 minLength: 1 - pattern: ^[A-Za-z0-9._+@/:^~=\[\],-]+$ + pattern: ^[A-Za-z0-9._+@/:^~=\[\],<>?*|-]+$ PackageRequest: description: Canonical request sent by a package broker client to the elevated broker. type: object diff --git a/policies/rust/now-policy-api/src/lib.rs b/policies/rust/now-policy-api/src/lib.rs index aec68fa..871daaf 100644 --- a/policies/rust/now-policy-api/src/lib.rs +++ b/policies/rust/now-policy-api/src/lib.rs @@ -399,8 +399,8 @@ impl From<&str> for RuleId { /// Package identifier string. /// -/// Validated against an explicit allowlist of characters actually used by the -/// supported package managers: ASCII alphanumerics plus `. - _ + @ / : ^ ~ = [ ] ,`. +/// Validated against an explicit allowlist of characters: ASCII alphanumerics +/// plus `. - _ + @ / : ^ ~ = [ ] , < > ? * |`. /// /// - `.`, `-`, `_`, `+`: winget (`Notepad++.Notepad++`), chocolatey, pip, cargo, /// dotnet, apt/dnf/pacman (`g++`, `libstdc++6`), PowerShell modules; @@ -411,15 +411,18 @@ impl From<&str> for RuleId { /// - `:`: npm aliases (`alias:@scope/package@^1.0.0`), vcpkg triplets /// (`curl:x64-windows`); /// -/// - `^`, `~`, `=`: version pins/ranges inside npm aliases and pip specifiers; +/// - `^`, `~`, `=`, `<`, `>`, `|`: version pins/ranges in npm aliases and pip +/// specifiers (`>=7 <8` without the space, `||` alternation); /// /// - `[`, `]`, `,`: vcpkg features (`curl[ssl,http2]:x64-windows`), pip extras -/// (`requests[socks]`). +/// (`requests[socks]`); /// -/// Everything else — including whitespace, control characters, wildcards -/// (`*`, `?`; policy-side package identifier matching is wildcard-based, so -/// wildcards in request identifiers would be ambiguous), shell metacharacters, -/// and non-ASCII — is rejected. +/// - `*`, `?`: wildcards. Note: policy-side package identifier matching is +/// wildcard-based, so wildcards in request identifiers may behave +/// surprisingly when matched against policy rules. +/// +/// Everything else — whitespace, control characters, `"`, `\`, shell +/// metacharacters, and non-ASCII — is rejected. #[derive( Debug, Clone, @@ -436,7 +439,7 @@ impl From<&str> for RuleId { #[deref(forward)] #[display("{_0}")] pub struct PackageIdentifier( - #[schemars(length(min = 1, max = 256), regex(pattern = r"^[A-Za-z0-9._+@/:^~=\[\],-]+$"))] pub String, + #[schemars(length(min = 1, max = 256), regex(pattern = r"^[A-Za-z0-9._+@/:^~=\[\],<>?*|-]+$"))] pub String, ); impl PackageIdentifier { @@ -459,12 +462,28 @@ impl PackageIdentifier { b.is_ascii_alphanumeric() || matches!( b, - b'.' | b'-' | b'_' | b'+' | b'@' | b'/' | b':' | b'^' | b'~' | b'=' | b'[' | b']' | b',' + b'.' | b'-' + | b'_' + | b'+' + | b'@' + | b'/' + | b':' + | b'^' + | b'~' + | b'=' + | b'[' + | b']' + | b',' + | b'<' + | b'>' + | b'?' + | b'*' + | b'|' ) }) { return Err(ModelValidationError::Invalid { type_name: "PackageIdentifier", - reason: "must contain only ASCII alphanumerics or '. - _ + @ / : ^ ~ = [ ] ,'".to_owned(), + reason: "must contain only ASCII alphanumerics or '. - _ + @ / : ^ ~ = [ ] , < > ? * |'".to_owned(), }); } @@ -592,6 +611,12 @@ mod tests { "dotnet-ef", "serde_json", "g++", + // Version range operators and wildcards. + "ranged:react@>=18.2.0", + "either:foo@1.0.0||2.0.0", + "Microsoft.*", + "some?id", + "spec<2.0", ]; for id in valid { @@ -609,12 +634,7 @@ mod tests { let invalid = [ "", "foo\\bar", - "foo*", - "foo?", "foo\"bar", - "foobar", - "foo|bar", "foo\r\nbar", "foo\tbar", "foo bar", @@ -692,7 +712,7 @@ mod tests { "Operation": "Install", "Manager": "Winget", "Source": { "Name": "winget" }, - "Package": { "Id": "evil|package\r\n" }, + "Package": { "Id": "evil;package\r\n" }, "Options": { "Interactive": false, "SkipHashCheck": false, From 57df96ed5c6097ee20a8b985bce3377d4f152417 Mon Sep 17 00:00:00 2001 From: Vladyslav Nikonov Date: Thu, 13 Aug 2026 13:38:31 +0300 Subject: [PATCH 5/9] docs(now-policy-api): list rejected characters precisely in PackageIdentifier docs Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- policies/rust/now-policy-api/openapi/now-policy-api.yaml | 2 +- policies/rust/now-policy-api/src/lib.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/policies/rust/now-policy-api/openapi/now-policy-api.yaml b/policies/rust/now-policy-api/openapi/now-policy-api.yaml index 32f5845..e596218 100644 --- a/policies/rust/now-policy-api/openapi/now-policy-api.yaml +++ b/policies/rust/now-policy-api/openapi/now-policy-api.yaml @@ -763,7 +763,7 @@ components: - `*`, `?`: wildcards. Note: policy-side package identifier matching is wildcard-based, so wildcards in request identifiers may behave surprisingly when matched against policy rules. - Everything else — whitespace, control characters, `"`, `\`, shell metacharacters, and non-ASCII — is rejected. + Everything else — whitespace, control characters, `"`, `\`, backtick, `! # $ % & ' ( ) ; { }`, and non-ASCII — is rejected. type: string maxLength: 256 minLength: 1 diff --git a/policies/rust/now-policy-api/src/lib.rs b/policies/rust/now-policy-api/src/lib.rs index 871daaf..194f5aa 100644 --- a/policies/rust/now-policy-api/src/lib.rs +++ b/policies/rust/now-policy-api/src/lib.rs @@ -421,8 +421,8 @@ impl From<&str> for RuleId { /// wildcard-based, so wildcards in request identifiers may behave /// surprisingly when matched against policy rules. /// -/// Everything else — whitespace, control characters, `"`, `\`, shell -/// metacharacters, and non-ASCII — is rejected. +/// Everything else — whitespace, control characters, `"`, `\`, backtick, +/// `! # $ % & ' ( ) ; { }`, and non-ASCII — is rejected. #[derive( Debug, Clone, From 0dbe4986cf42cf1e8b907d5405d6e7fc47f0525c Mon Sep 17 00:00:00 2001 From: Vladyslav Nikonov Date: Thu, 13 Aug 2026 13:44:43 +0300 Subject: [PATCH 6/9] fix(now-policy-api): reject wildcards in PackageIdentifier allowlist Policy-side package identifier matching is wildcard-based, so '*' and '?' in request identifiers would be ambiguous; keep them rejected while retaining '< > |' for version range operators. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../openapi/now-policy-api.yaml | 10 +++---- policies/rust/now-policy-api/src/lib.rs | 26 ++++++++----------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/policies/rust/now-policy-api/openapi/now-policy-api.yaml b/policies/rust/now-policy-api/openapi/now-policy-api.yaml index e596218..22567fe 100644 --- a/policies/rust/now-policy-api/openapi/now-policy-api.yaml +++ b/policies/rust/now-policy-api/openapi/now-policy-api.yaml @@ -749,7 +749,7 @@ components: description: |- Package identifier string. - Validated against an explicit allowlist of characters: ASCII alphanumerics plus `. - _ + @ / : ^ ~ = [ ] , < > ? * |`. + Validated against an explicit allowlist of characters: ASCII alphanumerics plus `. - _ + @ / : ^ ~ = [ ] , < > |`. - `.`, `-`, `_`, `+`: winget (`Notepad++.Notepad++`), chocolatey, pip, cargo, dotnet, apt/dnf/pacman (`g++`, `libstdc++6`), PowerShell modules; @@ -759,15 +759,13 @@ components: - `^`, `~`, `=`, `<`, `>`, `|`: version pins/ranges in npm aliases and pip specifiers (`>=7 <8` without the space, `||` alternation); - - `[`, `]`, `,`: vcpkg features (`curl[ssl,http2]:x64-windows`), pip extras (`requests[socks]`); + - `[`, `]`, `,`: vcpkg features (`curl[ssl,http2]:x64-windows`), pip extras (`requests[socks]`). - - `*`, `?`: wildcards. Note: policy-side package identifier matching is wildcard-based, so wildcards in request identifiers may behave surprisingly when matched against policy rules. - - Everything else — whitespace, control characters, `"`, `\`, backtick, `! # $ % & ' ( ) ; { }`, and non-ASCII — is rejected. + The wildcards `*` and `?` are rejected: policy-side package identifier matching is wildcard-based, so wildcards in request identifiers would be ambiguous. Everything else — whitespace, control characters, `"`, `\`, backtick, `! # $ % & ' ( ) ; { }`, and non-ASCII — is also rejected. type: string maxLength: 256 minLength: 1 - pattern: ^[A-Za-z0-9._+@/:^~=\[\],<>?*|-]+$ + pattern: ^[A-Za-z0-9._+@/:^~=\[\],<>|-]+$ PackageRequest: description: Canonical request sent by a package broker client to the elevated broker. type: object diff --git a/policies/rust/now-policy-api/src/lib.rs b/policies/rust/now-policy-api/src/lib.rs index 194f5aa..1b916b3 100644 --- a/policies/rust/now-policy-api/src/lib.rs +++ b/policies/rust/now-policy-api/src/lib.rs @@ -400,7 +400,7 @@ impl From<&str> for RuleId { /// Package identifier string. /// /// Validated against an explicit allowlist of characters: ASCII alphanumerics -/// plus `. - _ + @ / : ^ ~ = [ ] , < > ? * |`. +/// plus `. - _ + @ / : ^ ~ = [ ] , < > |`. /// /// - `.`, `-`, `_`, `+`: winget (`Notepad++.Notepad++`), chocolatey, pip, cargo, /// dotnet, apt/dnf/pacman (`g++`, `libstdc++6`), PowerShell modules; @@ -415,14 +415,12 @@ impl From<&str> for RuleId { /// specifiers (`>=7 <8` without the space, `||` alternation); /// /// - `[`, `]`, `,`: vcpkg features (`curl[ssl,http2]:x64-windows`), pip extras -/// (`requests[socks]`); +/// (`requests[socks]`). /// -/// - `*`, `?`: wildcards. Note: policy-side package identifier matching is -/// wildcard-based, so wildcards in request identifiers may behave -/// surprisingly when matched against policy rules. -/// -/// Everything else — whitespace, control characters, `"`, `\`, backtick, -/// `! # $ % & ' ( ) ; { }`, and non-ASCII — is rejected. +/// The wildcards `*` and `?` are rejected: policy-side package identifier +/// matching is wildcard-based, so wildcards in request identifiers would be +/// ambiguous. Everything else — whitespace, control characters, `"`, `\`, +/// backtick, `! # $ % & ' ( ) ; { }`, and non-ASCII — is also rejected. #[derive( Debug, Clone, @@ -439,7 +437,7 @@ impl From<&str> for RuleId { #[deref(forward)] #[display("{_0}")] pub struct PackageIdentifier( - #[schemars(length(min = 1, max = 256), regex(pattern = r"^[A-Za-z0-9._+@/:^~=\[\],<>?*|-]+$"))] pub String, + #[schemars(length(min = 1, max = 256), regex(pattern = r"^[A-Za-z0-9._+@/:^~=\[\],<>|-]+$"))] pub String, ); impl PackageIdentifier { @@ -476,14 +474,12 @@ impl PackageIdentifier { | b',' | b'<' | b'>' - | b'?' - | b'*' | b'|' ) }) { return Err(ModelValidationError::Invalid { type_name: "PackageIdentifier", - reason: "must contain only ASCII alphanumerics or '. - _ + @ / : ^ ~ = [ ] , < > ? * |'".to_owned(), + reason: "must contain only ASCII alphanumerics or '. - _ + @ / : ^ ~ = [ ] , < > |'".to_owned(), }); } @@ -611,11 +607,9 @@ mod tests { "dotnet-ef", "serde_json", "g++", - // Version range operators and wildcards. + // Version range operators. "ranged:react@>=18.2.0", "either:foo@1.0.0||2.0.0", - "Microsoft.*", - "some?id", "spec<2.0", ]; @@ -635,6 +629,8 @@ mod tests { "", "foo\\bar", "foo\"bar", + "foo*", + "foo?", "foo\r\nbar", "foo\tbar", "foo bar", From 9dc9270f478b82dfeed0ebae660ce8d7c9c83b49 Mon Sep 17 00:00:00 2001 From: Vladyslav Nikonov Date: Thu, 13 Aug 2026 14:11:53 +0300 Subject: [PATCH 7/9] fix(now-policy-api): add '# $ % { }' to PackageIdentifier allowlist Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../openapi/now-policy-api.yaml | 10 +++--- policies/rust/now-policy-api/src/lib.rs | 31 +++++++++++++------ 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/policies/rust/now-policy-api/openapi/now-policy-api.yaml b/policies/rust/now-policy-api/openapi/now-policy-api.yaml index 22567fe..fa68cdb 100644 --- a/policies/rust/now-policy-api/openapi/now-policy-api.yaml +++ b/policies/rust/now-policy-api/openapi/now-policy-api.yaml @@ -749,7 +749,7 @@ components: description: |- Package identifier string. - Validated against an explicit allowlist of characters: ASCII alphanumerics plus `. - _ + @ / : ^ ~ = [ ] , < > |`. + Validated against an explicit allowlist of characters: ASCII alphanumerics plus `. - _ + @ / : ^ ~ = [ ] , < > | # $ % { }`. - `.`, `-`, `_`, `+`: winget (`Notepad++.Notepad++`), chocolatey, pip, cargo, dotnet, apt/dnf/pacman (`g++`, `libstdc++6`), PowerShell modules; @@ -759,13 +759,15 @@ components: - `^`, `~`, `=`, `<`, `>`, `|`: version pins/ranges in npm aliases and pip specifiers (`>=7 <8` without the space, `||` alternation); - - `[`, `]`, `,`: vcpkg features (`curl[ssl,http2]:x64-windows`), pip extras (`requests[socks]`). + - `[`, `]`, `,`: vcpkg features (`curl[ssl,http2]:x64-windows`), pip extras (`requests[socks]`); - The wildcards `*` and `?` are rejected: policy-side package identifier matching is wildcard-based, so wildcards in request identifiers would be ambiguous. Everything else — whitespace, control characters, `"`, `\`, backtick, `! # $ % & ' ( ) ; { }`, and non-ASCII — is also rejected. + - `#`, `$`, `%`, `{`, `}`: additional identifier punctuation. + + The wildcards `*` and `?` are rejected: policy-side package identifier matching is wildcard-based, so wildcards in request identifiers would be ambiguous. Everything else — whitespace, control characters, `"`, `\`, backtick, `! & ' ( ) ;`, and non-ASCII — is also rejected. type: string maxLength: 256 minLength: 1 - pattern: ^[A-Za-z0-9._+@/:^~=\[\],<>|-]+$ + pattern: ^[A-Za-z0-9._+@/:^~=\[\],<>|#$%{}-]+$ PackageRequest: description: Canonical request sent by a package broker client to the elevated broker. type: object diff --git a/policies/rust/now-policy-api/src/lib.rs b/policies/rust/now-policy-api/src/lib.rs index 1b916b3..92f4dbb 100644 --- a/policies/rust/now-policy-api/src/lib.rs +++ b/policies/rust/now-policy-api/src/lib.rs @@ -400,7 +400,7 @@ impl From<&str> for RuleId { /// Package identifier string. /// /// Validated against an explicit allowlist of characters: ASCII alphanumerics -/// plus `. - _ + @ / : ^ ~ = [ ] , < > |`. +/// plus `. - _ + @ / : ^ ~ = [ ] , < > | # $ % { }`. /// /// - `.`, `-`, `_`, `+`: winget (`Notepad++.Notepad++`), chocolatey, pip, cargo, /// dotnet, apt/dnf/pacman (`g++`, `libstdc++6`), PowerShell modules; @@ -415,12 +415,14 @@ impl From<&str> for RuleId { /// specifiers (`>=7 <8` without the space, `||` alternation); /// /// - `[`, `]`, `,`: vcpkg features (`curl[ssl,http2]:x64-windows`), pip extras -/// (`requests[socks]`). +/// (`requests[socks]`); +/// +/// - `#`, `$`, `%`, `{`, `}`: additional identifier punctuation. /// /// The wildcards `*` and `?` are rejected: policy-side package identifier /// matching is wildcard-based, so wildcards in request identifiers would be /// ambiguous. Everything else — whitespace, control characters, `"`, `\`, -/// backtick, `! # $ % & ' ( ) ; { }`, and non-ASCII — is also rejected. +/// backtick, `! & ' ( ) ;`, and non-ASCII — is also rejected. #[derive( Debug, Clone, @@ -437,7 +439,11 @@ impl From<&str> for RuleId { #[deref(forward)] #[display("{_0}")] pub struct PackageIdentifier( - #[schemars(length(min = 1, max = 256), regex(pattern = r"^[A-Za-z0-9._+@/:^~=\[\],<>|-]+$"))] pub String, + #[schemars( + length(min = 1, max = 256), + regex(pattern = r"^[A-Za-z0-9._+@/:^~=\[\],<>|#$%{}-]+$") + )] + pub String, ); impl PackageIdentifier { @@ -475,11 +481,17 @@ impl PackageIdentifier { | b'<' | b'>' | b'|' + | b'#' + | b'$' + | b'%' + | b'{' + | b'}' ) }) { return Err(ModelValidationError::Invalid { type_name: "PackageIdentifier", - reason: "must contain only ASCII alphanumerics or '. - _ + @ / : ^ ~ = [ ] , < > |'".to_owned(), + reason: "must contain only ASCII alphanumerics or '. - _ + @ / : ^ ~ = [ ] , < > | # $ % { }'" + .to_owned(), }); } @@ -611,6 +623,11 @@ mod tests { "ranged:react@>=18.2.0", "either:foo@1.0.0||2.0.0", "spec<2.0", + // Additional identifier punctuation. + "foo#bar", + "foo$bar", + "foo%20bar", + "foo{bar}", ]; for id in valid { @@ -637,14 +654,10 @@ mod tests { "foo\u{0}bar", "foo\u{7f}bar", "foo!bar", - "foo#bar", - "foo$bar", - "foo%bar", "foo&bar", "foo'bar", "foo(bar)", "foo;bar", - "foo{bar}", "foo`bar", "gr\u{fc}n", &"a".repeat(257), From e4df9537c39bba104c9782291433743842615aa2 Mon Sep 17 00:00:00 2001 From: Vladyslav Nikonov Date: Thu, 13 Aug 2026 14:20:12 +0300 Subject: [PATCH 8/9] fix(now-policy-api): exact-version PackageIdentifier allowlist with '# $ % { }' Per user feedback, the final allowlist is ASCII alphanumerics plus '. - _ + @ / : [ ] , # $ % { }'. Version range/pin operators ('< > = ! | ^ ~') are rejected: the broker matches against a specific, exact version carried in the request's separate Package.Version field, so range expressions do not belong in the identifier. npm aliases must use exact versions (e.g. 'alias:pkg@7.20.0'). Wildcards '* ?' remain rejected because policy-side identifier matching is wildcard-based. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../openapi/now-policy-api.yaml | 10 ++- policies/rust/now-policy-api/src/lib.rs | 62 +++++++++---------- 2 files changed, 32 insertions(+), 40 deletions(-) diff --git a/policies/rust/now-policy-api/openapi/now-policy-api.yaml b/policies/rust/now-policy-api/openapi/now-policy-api.yaml index fa68cdb..bfc5294 100644 --- a/policies/rust/now-policy-api/openapi/now-policy-api.yaml +++ b/policies/rust/now-policy-api/openapi/now-policy-api.yaml @@ -749,25 +749,23 @@ components: description: |- Package identifier string. - Validated against an explicit allowlist of characters: ASCII alphanumerics plus `. - _ + @ / : ^ ~ = [ ] , < > | # $ % { }`. + Validated against an explicit allowlist of characters: ASCII alphanumerics plus `. - _ + @ / : [ ] , # $ % { }`. - `.`, `-`, `_`, `+`: winget (`Notepad++.Notepad++`), chocolatey, pip, cargo, dotnet, apt/dnf/pacman (`g++`, `libstdc++6`), PowerShell modules; - `@`, `/`: scoped npm/Bun packages (`@scope/package`), homebrew and scoop `tap/formula` paths, versioned formulas (`python@3.11`); - - `:`: npm aliases (`alias:@scope/package@^1.0.0`), vcpkg triplets (`curl:x64-windows`); - - - `^`, `~`, `=`, `<`, `>`, `|`: version pins/ranges in npm aliases and pip specifiers (`>=7 <8` without the space, `||` alternation); + - `:`: npm aliases (`alias:@scope/package@1.0.0`), vcpkg triplets (`curl:x64-windows`); - `[`, `]`, `,`: vcpkg features (`curl[ssl,http2]:x64-windows`), pip extras (`requests[socks]`); - `#`, `$`, `%`, `{`, `}`: additional identifier punctuation. - The wildcards `*` and `?` are rejected: policy-side package identifier matching is wildcard-based, so wildcards in request identifiers would be ambiguous. Everything else — whitespace, control characters, `"`, `\`, backtick, `! & ' ( ) ;`, and non-ASCII — is also rejected. + Version range/pin operators (`<`, `>`, `=`, `!`, `|`, `^`, `~`) are rejected: the broker matches against a specific, exact version carried in the request's separate `Package.Version` field, so range expressions do not belong in the identifier (npm aliases must use exact versions, e.g. `alias:pkg@7.20.0`). The wildcards `*` and `?` are also rejected: policy-side package identifier matching is wildcard-based, so wildcards in request identifiers would be ambiguous. Everything else — whitespace, control characters, `"`, `\`, backtick, `& ' ( ) ;`, and non-ASCII — is rejected as well. type: string maxLength: 256 minLength: 1 - pattern: ^[A-Za-z0-9._+@/:^~=\[\],<>|#$%{}-]+$ + pattern: ^[A-Za-z0-9._+@/:\[\],#$%{}-]+$ PackageRequest: description: Canonical request sent by a package broker client to the elevated broker. type: object diff --git a/policies/rust/now-policy-api/src/lib.rs b/policies/rust/now-policy-api/src/lib.rs index 92f4dbb..78ff4bd 100644 --- a/policies/rust/now-policy-api/src/lib.rs +++ b/policies/rust/now-policy-api/src/lib.rs @@ -400,7 +400,7 @@ impl From<&str> for RuleId { /// Package identifier string. /// /// Validated against an explicit allowlist of characters: ASCII alphanumerics -/// plus `. - _ + @ / : ^ ~ = [ ] , < > | # $ % { }`. +/// plus `. - _ + @ / : [ ] , # $ % { }`. /// /// - `.`, `-`, `_`, `+`: winget (`Notepad++.Notepad++`), chocolatey, pip, cargo, /// dotnet, apt/dnf/pacman (`g++`, `libstdc++6`), PowerShell modules; @@ -408,21 +408,23 @@ impl From<&str> for RuleId { /// - `@`, `/`: scoped npm/Bun packages (`@scope/package`), homebrew and scoop /// `tap/formula` paths, versioned formulas (`python@3.11`); /// -/// - `:`: npm aliases (`alias:@scope/package@^1.0.0`), vcpkg triplets +/// - `:`: npm aliases (`alias:@scope/package@1.0.0`), vcpkg triplets /// (`curl:x64-windows`); /// -/// - `^`, `~`, `=`, `<`, `>`, `|`: version pins/ranges in npm aliases and pip -/// specifiers (`>=7 <8` without the space, `||` alternation); -/// /// - `[`, `]`, `,`: vcpkg features (`curl[ssl,http2]:x64-windows`), pip extras /// (`requests[socks]`); /// /// - `#`, `$`, `%`, `{`, `}`: additional identifier punctuation. /// -/// The wildcards `*` and `?` are rejected: policy-side package identifier -/// matching is wildcard-based, so wildcards in request identifiers would be -/// ambiguous. Everything else — whitespace, control characters, `"`, `\`, -/// backtick, `! & ' ( ) ;`, and non-ASCII — is also rejected. +/// Version range/pin operators (`<`, `>`, `=`, `!`, `|`, `^`, `~`) are +/// rejected: the broker matches against a specific, exact version carried in +/// the request's separate `Package.Version` field, so range expressions do +/// not belong in the identifier (npm aliases must use exact versions, e.g. +/// `alias:pkg@7.20.0`). The wildcards `*` and `?` are also rejected: +/// policy-side package identifier matching is wildcard-based, so wildcards in +/// request identifiers would be ambiguous. Everything else — whitespace, +/// control characters, `"`, `\`, backtick, `& ' ( ) ;`, and non-ASCII — is +/// rejected as well. #[derive( Debug, Clone, @@ -439,11 +441,7 @@ impl From<&str> for RuleId { #[deref(forward)] #[display("{_0}")] pub struct PackageIdentifier( - #[schemars( - length(min = 1, max = 256), - regex(pattern = r"^[A-Za-z0-9._+@/:^~=\[\],<>|#$%{}-]+$") - )] - pub String, + #[schemars(length(min = 1, max = 256), regex(pattern = r"^[A-Za-z0-9._+@/:\[\],#$%{}-]+$"))] pub String, ); impl PackageIdentifier { @@ -472,15 +470,9 @@ impl PackageIdentifier { | b'@' | b'/' | b':' - | b'^' - | b'~' - | b'=' | b'[' | b']' | b',' - | b'<' - | b'>' - | b'|' | b'#' | b'$' | b'%' @@ -490,8 +482,7 @@ impl PackageIdentifier { }) { return Err(ModelValidationError::Invalid { type_name: "PackageIdentifier", - reason: "must contain only ASCII alphanumerics or '. - _ + @ / : ^ ~ = [ ] , < > | # $ % { }'" - .to_owned(), + reason: "must contain only ASCII alphanumerics or '. - _ + @ / : [ ] , # $ % { }'".to_owned(), }); } @@ -600,10 +591,10 @@ mod tests { // Scoped npm/Bun packages. "@scope/package", "@babel/core", - // npm aliases. - "babel-core-legacy:@babel/core@^7.20.0", - "my-alias:lodash@~4.17.21", - "pinned:react@=18.2.0", + // npm aliases (exact versions only). + "babel-core-legacy:@babel/core@7.20.0", + "my-alias:lodash@4.17.21", + "pinned:react@18.2.0", // vcpkg triplets and features. "curl:x64-windows", "curl[ssl]:x64-windows", @@ -611,18 +602,13 @@ mod tests { // Homebrew/scoop tap paths and versioned formulas. "extras/vscode", "homebrew/core/python@3.11", - // pip extras and version pins. + // pip extras. "requests[socks]", - "requests==2.32.0", // Chocolatey, dotnet, cargo, apt-style names. "git.install", "dotnet-ef", "serde_json", "g++", - // Version range operators. - "ranged:react@>=18.2.0", - "either:foo@1.0.0||2.0.0", - "spec<2.0", // Additional identifier punctuation. "foo#bar", "foo$bar", @@ -653,12 +639,20 @@ mod tests { "foo bar", "foo\u{0}bar", "foo\u{7f}bar", - "foo!bar", "foo&bar", "foo'bar", "foo(bar)", "foo;bar", "foo`bar", + // Version range/pin operators (exact versions only, carried in Package.Version). + "babel-core-legacy:@babel/core@^7.20.0", + "my-alias:lodash@~4.17.21", + "pinned:react@=18.2.0", + "requests==2.32.0", + "requests!=2.32.0", + "ranged:react@>=18.2.0", + "either:foo@1.0.0||2.0.0", + "spec<2.0", "gr\u{fc}n", &"a".repeat(257), ]; @@ -676,7 +670,7 @@ mod tests { fn package_request_deserializes_scoped_and_aliased_identifiers() { let identifiers = [ "@scope/package", - "babel-core-legacy:@babel/core@^7.20.0", + "babel-core-legacy:@babel/core@7.20.0", "curl:x64-windows", "curl[ssl]:x64-windows", ]; From 8cb3c58c06936fc861faf816b90d8c866951c643 Mon Sep 17 00:00:00 2001 From: Vladyslav Nikonov Date: Thu, 13 Aug 2026 14:25:06 +0300 Subject: [PATCH 9/9] docs(now-policy-api): document shell-expansion caveat for '# $ % { }' Note in the PackageIdentifier doc comment (and the generated schema description) that these allowlisted characters carry expansion semantics in some shells, so downstream command builders must pass identifiers as discrete process arguments rather than interpolating them into a shell command line. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- policies/rust/now-policy-api/openapi/now-policy-api.yaml | 2 +- policies/rust/now-policy-api/src/lib.rs | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/policies/rust/now-policy-api/openapi/now-policy-api.yaml b/policies/rust/now-policy-api/openapi/now-policy-api.yaml index bfc5294..ba875ec 100644 --- a/policies/rust/now-policy-api/openapi/now-policy-api.yaml +++ b/policies/rust/now-policy-api/openapi/now-policy-api.yaml @@ -759,7 +759,7 @@ components: - `[`, `]`, `,`: vcpkg features (`curl[ssl,http2]:x64-windows`), pip extras (`requests[socks]`); - - `#`, `$`, `%`, `{`, `}`: additional identifier punctuation. + - `#`, `$`, `%`, `{`, `}`: additional identifier punctuation (accepted by product decision for forward compatibility). Caveat: these characters carry expansion semantics in some shells (`${VAR}`, `%VAR%`, brace expansion), so downstream command builders must pass identifiers as discrete process arguments and never interpolate them into a shell command line. Version range/pin operators (`<`, `>`, `=`, `!`, `|`, `^`, `~`) are rejected: the broker matches against a specific, exact version carried in the request's separate `Package.Version` field, so range expressions do not belong in the identifier (npm aliases must use exact versions, e.g. `alias:pkg@7.20.0`). The wildcards `*` and `?` are also rejected: policy-side package identifier matching is wildcard-based, so wildcards in request identifiers would be ambiguous. Everything else — whitespace, control characters, `"`, `\`, backtick, `& ' ( ) ;`, and non-ASCII — is rejected as well. type: string diff --git a/policies/rust/now-policy-api/src/lib.rs b/policies/rust/now-policy-api/src/lib.rs index 78ff4bd..4b6b8d3 100644 --- a/policies/rust/now-policy-api/src/lib.rs +++ b/policies/rust/now-policy-api/src/lib.rs @@ -414,7 +414,12 @@ impl From<&str> for RuleId { /// - `[`, `]`, `,`: vcpkg features (`curl[ssl,http2]:x64-windows`), pip extras /// (`requests[socks]`); /// -/// - `#`, `$`, `%`, `{`, `}`: additional identifier punctuation. +/// - `#`, `$`, `%`, `{`, `}`: additional identifier punctuation (accepted by +/// product decision for forward compatibility). Caveat: these characters +/// carry expansion semantics in some shells (`${VAR}`, `%VAR%`, brace +/// expansion), so downstream command builders must pass identifiers as +/// discrete process arguments and never interpolate them into a shell +/// command line. /// /// Version range/pin operators (`<`, `>`, `=`, `!`, `|`, `^`, `~`) are /// rejected: the broker matches against a specific, exact version carried in