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)
Loading…
How AI contributed
Incomplete remediationKiota 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.
Only the highlighted steps are this advisory. The first card 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 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.
Fixed again
This is the patch that actually stops the same attack path.
AI-assisted fix: GitHub Copilot · Gavin Barron
Code comparison
--- 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));Candidate 1032b2895f8c7af3b7a3a0d27cdca20404334600a57f0b1c72a7b3d146e3ca36 · Fix 4fa23f6fe64cf058e46262c7babbfddb7a9d43c3e0b58e38914df5849bb7ca55
Releases
Advisory references