Original flaw
Earlier flawEncoded file references could bypass Kiota's safe-file validation.
Sink: Microsoft Kiota IsSafeFileReference percent-decoding path
Originally written by AI (Jingjing Jia)
How AI contributed: Incomplete remediationPublished Jul 24, 2026 in
microsoft/kiota
Impact Kiota generates AI plugin manifests from an OpenAPI description. When the description contains an x-ai-capabilities response semantics static_template (or the adaptive-card extension x-ai-adaptive-card), the file reference is written into the generated manifest's response_semantics.static_template.file and is later resolved by the AI host relative to the plugin package. An attacker who controls or tampers with the OpenAPI description consumed by Kiota can supply a file reference that resolves outside the manifest package (e.g. ../../../../etc/passwd, an absolute path, or a file:// / http(s):// URI). When the generated manifest is deployed and consumed by an AI host, this can lead to inclusion or disclosure of files outside the intended package boundary (CWE-22 Path Traversal, CWE-829 Inclusion of Functionality from an Untrusted Control Sphere). A mitigation shipped in v1.32.5 (ExtensionResponseSemanticsStaticTemplate.IsSafeFileReference) rejected literal traversal, rooted paths, drive-qualified paths, and absolute URIs. However, that check inspected the raw reference string, so percent-encoded payloads bypassed every check and were still emitted verbatim. Examples that were incorrectly accepted as safe: Input: Decodes to %2e%2e/card.json: ../card.json ..%2f..%2f..%2f..%2f..%2f..%2fetc%2fpasswd: ../../../../../../etc/passwd file%3A%2F%2F%2Fetc%2Fpasswd: file:///etc/passwd %2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd: ../../../etc/passwd Multi-level (double) encoding such as %252e%252e%252fcard.json was also affected. A follow-up review found additional residual bypasses of the same validator: an embedded NUL byte (%00) that truncated the path and defeated the parent-directory segment check, encoding nested deeper than the decode budget (which failed open), and Unicode full-width homoglyphs (e.g. %EF%BC%8E%EF%BC%8E → ..). Patches · The percent-encoding bypass is fixed by decoding the reference (bounded multi-pass) before validation: https://github.com/microsoft/kiota/pull/7910 · Residual bypasses (NUL / control characters, decode-budget fail-open, Unicode homoglyphs) are fixed by failing closed on residual encoding, rejecting control characters, and NFKC-folding before validation: https://github.com/microsoft/kiota/pull/7913 (tracking issue https://github.com/microsoft/kiota/issues/7912) Users should upgrade to the first released Microsoft.OpenApi.Kiota version that includes these fixes (the release following 1.33.0). Workarounds · Only generate clients/plugins from trusted OpenAPI descriptions. · Review generated plugin manifests before deployment and reject any response_semantics.static_template.file value that is not a simple relative path within the adaptiveCards/ package folder (no .., no rooted/absolute paths, no URIs, no percent-encoded separators). References · CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') · CWE-829: Inclusion of Functionality from an Untrusted Control Sphere · Affected code: src/Kiota.Builder/OpenApiExtensions/OpenApiAiCapabilitiesExtension.cs (IsSafeFileReference) and enforcement in src/Kiota.Builder/Plugins/PluginsGenerationService.cs.
The v1.32.5 check in ExtensionResponseSemanticsStaticTemplate.IsSafeFileReference compared the raw file reference, so encoded traversal such as %2e%2e/card.json was written into the generated plugin manifest, which the AI host resolves relative to the package. The Copilot-assisted decoding change that followed was incomplete too: double encoding, an embedded NUL byte and full-width homoglyphs passed until validation failed closed.
Case facts
Only the highlighted steps are this advisory. The first step is the earlier flaw the AI tried, and failed, to close.
Original flaw
Earlier flawSink: Microsoft Kiota IsSafeFileReference percent-decoding path
Originally written by AI (Jingjing Jia)
This advisoryGHSA-P5RM-JG5C-8C77
AI tried to fix this
The AI-assisted change was a real security patch, but it left the same advisory reachable.
Missed: Retained a fail-open path when decoding or validation did not produce a safe result.
Incomplete AI-assisted fix · GitHub Copilot
Fixed again
This is the patch that actually stops the same attack path.
AI-assisted fix: GitHub Copilot · Gavin Barron
--- a/src/Kiota.Builder/OpenApiExtensions/OpenApiAiCapabilitiesExtension.cs+++ b/src/Kiota.Builder/OpenApiExtensions/OpenApiAiCapabilitiesExtension.cs@@ -386,8 +386,13 @@ public class ExtensionResponseSemanticsStaticTemplate // Inlined cards (no "file" property) are always considered safe. public bool HasUnsafeFileReference => File is not null && !IsSafeFileReference(File); + // Upper bound on percent-decode passes; enough to defeat multi-level (double) encoding without unbounded looping.+ private const int MaxPercentDecodePasses = 5;+ // Validates that a static_template file reference is a relative path that cannot point outside the manifest // package: rejects absolute URIs, POSIX/UNC rooted paths, Windows drive paths, and '..' traversal (CWE-22/CWE-829).+ // The reference is percent-decoded first so encoded traversal sequences (e.g. %2e%2e for '..', %2f for '/',+ // %3a for ':') cannot bypass the checks below; decoding is repeated to defeat multi-level encoding. public static bool IsSafeFileReference(string? file) { if (string.IsNullOrWhiteSpace(file))@@ -395,14 +400,32 @@ public class ExtensionResponseSemanticsStaticTemplate return false; } + // Percent-decode repeatedly until the value is stable so encoded (and double-encoded) traversal payloads+ // are normalized back to their literal form before validation.+ var decoded = file;+ for (var pass = 0; pass < MaxPercentDecodePasses; pass++)+ {+ var next = Uri.UnescapeDataString(decoded);+ if (string.Equals(next, decoded, StringComparison.Ordinal))+ {+ break;+ }+ decoded = next;+ }++ if (string.IsNullOrWhiteSpace(decoded))+ {+ return false;+ }+ // The manifest schema requires a relative file path; reject absolute URIs such as http(s):// or file://.- if (Uri.TryCreate(file, UriKind.Absolute, out _))+ if (Uri.TryCreate(decoded, UriKind.Absolute, out _)) { return false; } // Normalize separators so the checks below are OS-independent (the manifest is consumed on any platform).- var normalized = file.Replace('\\', '/');+ var normalized = decoded.Replace('\\', '/'); // Reject POSIX-absolute and UNC-style rooted paths (e.g. /etc/passwd, //server/share). if (normalized.StartsWith('/'))--- a/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiAiCapabilitiesExtensionTests.cs+++ b/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiAiCapabilitiesExtensionTests.cs@@ -269,6 +269,18 @@ components: [InlineData("http://attacker.example/exfil", false)] [InlineData("https://attacker.example/card.json", false)] [InlineData("file:///etc/passwd", false)]+ // Percent-encoded traversal / URIs must be decoded before validation (CWE-22 / CWE-829).+ [InlineData("%2e%2e/card.json", false)]+ [InlineData("..%2f..%2f..%2f..%2f..%2f..%2fetc%2fpasswd", false)]+ [InlineData("file%3A%2F%2F%2Fetc%2Fpasswd", false)]+ [InlineData("%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd", false)]+ // Encoding hardening variants.+ [InlineData("%2E%2E/card.json", false)]+ [InlineData("..%5c..%5csecret.json", false)]+ [InlineData("https%3A%2F%2Fattacker.example%2Fcard.json", false)]+ [InlineData("%252e%252e%252fcard.json", false)]+ // A benign filename containing an encoded space stays safe after decoding.+ [InlineData("card%20name.json", true)] public void StaticTemplateIsSafeFileReferenceValidatesPaths(string file, bool expectedSafe) { Assert.Equal(expectedSafe, ExtensionResponseSemanticsStaticTemplate.IsSafeFileReference(file));--- a/src/Kiota.Builder/OpenApiExtensions/OpenApiAiCapabilitiesExtension.cs+++ b/src/Kiota.Builder/OpenApiExtensions/OpenApiAiCapabilitiesExtension.cs@@ -1,5 +1,6 @@ using System; using System.Collections.Generic;+using System.Linq; using System.Text.Json; using System.Text.Json.Nodes; using Kiota.Builder.Extensions;@@ -392,7 +393,9 @@ public class ExtensionResponseSemanticsStaticTemplate // Validates that a static_template file reference is a relative path that cannot point outside the manifest // package: rejects absolute URIs, POSIX/UNC rooted paths, Windows drive paths, and '..' traversal (CWE-22/CWE-829). // The reference is percent-decoded first so encoded traversal sequences (e.g. %2e%2e for '..', %2f for '/',- // %3a for ':') cannot bypass the checks below; decoding is repeated to defeat multi-level encoding.+ // %3a for ':') cannot bypass the checks below; decoding is repeated to defeat multi-level encoding, residual+ // encoding beyond the decode budget fails closed, embedded control/NUL characters are rejected, and Unicode+ // compatibility forms are folded so full-width homoglyph traversal cannot slip through. public static bool IsSafeFileReference(string? file) { if (string.IsNullOrWhiteSpace(file))@@ -418,6 +421,26 @@ public class ExtensionResponseSemanticsStaticTemplate return false; } + // Fail closed if percent-encoding remains after the decode budget is exhausted: undecoded residue+ // (e.g. more encoding levels than MaxPercentDecodePasses) could still be decoded by the downstream+ // consumer into a traversal sequence, so treat it as unsafe rather than accepting it verbatim.+ if (!string.Equals(Uri.UnescapeDataString(decoded), decoded, StringComparison.Ordinal))+ {+ return false;+ }++ // Fold Unicode compatibility forms (e.g. full-width '.'/'/') to their canonical ASCII equivalents so+ // homoglyph traversal payloads are normalized before the checks below. Validation only; the original+ // reference is still emitted verbatim.+ decoded = decoded.Normalize(System.Text.NormalizationForm.FormKC);++ // Reject control characters (e.g. an embedded NUL from %00) which can truncate the path in downstream+ // consumers and defeat the parent-directory segment check below.+ if (decoded.Any(char.IsControl))+ {+ return false;+ }+ // The manifest schema requires a relative file path; reject absolute URIs such as http(s):// or file://. if (Uri.TryCreate(decoded, UriKind.Absolute, out _)) {--- a/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiAiCapabilitiesExtensionTests.cs+++ b/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiAiCapabilitiesExtensionTests.cs@@ -281,6 +281,15 @@ components: [InlineData("%252e%252e%252fcard.json", false)] // A benign filename containing an encoded space stays safe after decoding. [InlineData("card%20name.json", true)]+ // Encoded NUL / control characters must be rejected (truncation + segment-check evasion).+ [InlineData("card%00.json", false)]+ [InlineData("safe.json%00%2e%2e%2fetc%2fpasswd", false)]+ // Encoding deeper than the decode budget must fail closed rather than pass residual %XX through.+ [InlineData("%25252525252e%25252525252e%25252525252fx", false)]+ [InlineData("%2525252525252e%2525252525252e%2525252525252fx", false)]+ // Unicode full-width homoglyph traversal (literal and percent-encoded UTF-8) is folded and rejected.+ [InlineData("\uFF0E\uFF0E/card.json", false)]+ [InlineData("%EF%BC%8E%EF%BC%8E/card.json", false)] public void StaticTemplateIsSafeFileReferenceValidatesPaths(string file, bool expectedSafe) { Assert.Equal(expectedSafe, ExtensionResponseSemanticsStaticTemplate.IsSafeFileReference(file));AI-assisted change 1032b2895f8c7af3b7a3a0d27cdca20404334600a57f0b1c72a7b3d146e3ca36 · Fix 4fa23f6fe64cf058e46262c7babbfddb7a9d43c3e0b58e38914df5849bb7ca55