diff --git a/release_notes.md b/release_notes.md index 3ae6a5c80c..6a865bea6e 100644 --- a/release_notes.md +++ b/release_notes.md @@ -17,3 +17,4 @@ - Improvements to coldstart pipeline (#11102). - Update Python Worker Version to [4.38.0](https://github.com/Azure/azure-functions-python-worker/releases/tag/4.38.0) - Only start the Diagnostic Events flush logs timer when events are present, preventing unnecessary flush attempts (#11100). +- Improved metadata binding validation (#11101) diff --git a/src/WebJobs.Script.WebHost/Extensions/FunctionMetadataExtensions.cs b/src/WebJobs.Script.WebHost/Extensions/FunctionMetadataExtensions.cs index 2f35016d60..f45b4a8162 100644 --- a/src/WebJobs.Script.WebHost/Extensions/FunctionMetadataExtensions.cs +++ b/src/WebJobs.Script.WebHost/Extensions/FunctionMetadataExtensions.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; +using Microsoft.Azure.WebJobs.Logging; using Microsoft.Azure.WebJobs.Script.Description; using Microsoft.Azure.WebJobs.Script.Management.Models; using Microsoft.Azure.WebJobs.Script.WebHost.Management; @@ -175,7 +176,7 @@ private static async Task GetFunctionConfig(FunctionMetadata metadata, private static async Task GetFunctionConfigFromFile(string path) { - return JObject.Parse(await FileUtility.ReadAsync(path)); + return JObject.Parse(Sanitizer.Sanitize(await FileUtility.ReadAsync(path))); } private static JObject GetFunctionConfigFromMetadata(FunctionMetadata metadata) diff --git a/src/WebJobs.Script/Host/HostFunctionMetadataProvider.cs b/src/WebJobs.Script/Host/HostFunctionMetadataProvider.cs index a1b972a03d..fdbe15347c 100644 --- a/src/WebJobs.Script/Host/HostFunctionMetadataProvider.cs +++ b/src/WebJobs.Script/Host/HostFunctionMetadataProvider.cs @@ -9,6 +9,7 @@ using System.IO.Abstractions; using System.Linq; using System.Threading.Tasks; +using Microsoft.Azure.WebJobs.Logging; using Microsoft.Azure.WebJobs.Script.Configuration; using Microsoft.Azure.WebJobs.Script.Description; using Microsoft.Azure.WebJobs.Script.Diagnostics; @@ -135,7 +136,11 @@ internal static FunctionMetadata ParseFunctionMetadata(string functionName, JObj { foreach (JObject binding in bindingArray) { - BindingMetadata bindingMetadata = BindingMetadata.Create(binding); + // Sanitize the binding JSON to remove any sensitive information before creating BindingMetadata + var sanitizedBindingJson = Sanitizer.Sanitize(binding.ToString(Newtonsoft.Json.Formatting.None)); + var sanitizedJObject = JObject.Parse(sanitizedBindingJson); + + BindingMetadata bindingMetadata = BindingMetadata.Create(sanitizedJObject); functionMetadata.Bindings.Add(bindingMetadata); } } diff --git a/src/WebJobs.Script/Host/WorkerFunctionMetadataProvider.cs b/src/WebJobs.Script/Host/WorkerFunctionMetadataProvider.cs index fb8f61dc65..e31f74385e 100644 --- a/src/WebJobs.Script/Host/WorkerFunctionMetadataProvider.cs +++ b/src/WebJobs.Script/Host/WorkerFunctionMetadataProvider.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Reactive.Linq; using System.Threading.Tasks; +using Microsoft.Azure.WebJobs.Logging; using Microsoft.Azure.WebJobs.Script.Description; using Microsoft.Azure.WebJobs.Script.Diagnostics.Extensions; using Microsoft.Azure.WebJobs.Script.Workers.Rpc; @@ -257,7 +258,7 @@ internal FunctionMetadata ValidateBindings(IEnumerable rawBindings, Func foreach (string binding in rawBindings) { - var deserializedObj = JsonConvert.DeserializeObject(binding, _dateTimeSerializerSettings); + var deserializedObj = JsonConvert.DeserializeObject(Sanitizer.Sanitize(binding), _dateTimeSerializerSettings); var functionBinding = BindingMetadata.Create(deserializedObj); Utility.ValidateBinding(functionBinding); diff --git a/test/WebJobs.Script.Tests/HostFunctionMetadataProviderTests.cs b/test/WebJobs.Script.Tests/HostFunctionMetadataProviderTests.cs index f49ea826d2..b056e26b2b 100644 --- a/test/WebJobs.Script.Tests/HostFunctionMetadataProviderTests.cs +++ b/test/WebJobs.Script.Tests/HostFunctionMetadataProviderTests.cs @@ -350,5 +350,59 @@ public void ParseFunctionMetadata_ResolvesCorrectDotNetLanguage(string scriptFil var metadata = HostFunctionMetadataProvider.ParseFunctionMetadata("Function1", json, scriptRoot, fileSystemMock.Object, workerConfigs, functionsWorkerRuntime); Assert.Equal(expectedLanguage, metadata.Language); } + + [Fact] + public void ParseFunctionMetadata_MasksSensitiveDataInBindings() + { + const string functionJson = @"{ + ""scriptFile"": ""app.dll"", + ""bindings"": [ + { + ""name"": ""myQueueItem"", + ""type"": ""queueTrigger"", + ""direction"": ""in"", + ""queueName"": ""test-input-node"", + ""connection"": ""DefaultEndpointsProtocol=https;AccountName=a;AccountKey=b/c==;EndpointSuffix=core.windows.net"" + }, + { + ""name"": ""$return"", + ""type"": ""queue"", + ""direction"": ""out"", + ""queueName"": ""test-output-node"", + ""connection"": ""MyConnection"" + } + ] + }"; + + var json = JObject.Parse(functionJson); + var scriptRoot = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + + var fullFileSystem = new FileSystem(); + var fileSystemMock = new Mock(); + var fileBaseMock = new Mock(); + fileSystemMock.Setup(f => f.Path).Returns(fullFileSystem.Path); + fileSystemMock.Setup(f => f.File).Returns(fileBaseMock.Object); + fileBaseMock.Setup(f => f.Exists(It.IsAny())).Returns(true); + + IList workerConfigs = []; + + var metadata = HostFunctionMetadataProvider.ParseFunctionMetadata("Function1", json, scriptRoot, fileSystemMock.Object, workerConfigs, "custom"); + + Assert.NotNull(metadata); + Assert.NotNull(metadata.Bindings); + Assert.Equal(2, metadata.Bindings.Count); + + // The first binding should have its connection string replaced with "[Hidden Credential]" + var bindingMetadata1 = metadata.Bindings[0]; + Assert.Equal("[Hidden Credential]", bindingMetadata1.Connection); + Assert.Equal("[Hidden Credential]", bindingMetadata1.Raw["connection"]!.ToString()); + Assert.DoesNotContain("AccountKey", bindingMetadata1.Raw.ToString()); + + // The second binding should remain unchanged (named connection) + var outputBinding = metadata.Bindings[1]; + Assert.Equal("MyConnection", outputBinding.Connection); + Assert.Contains("MyConnection", outputBinding.Raw.ToString()); + Assert.DoesNotContain("[Hidden Credential]", outputBinding.Raw["connection"]!.ToString()); + } } } diff --git a/test/WebJobs.Script.Tests/SanitizerTests.cs b/test/WebJobs.Script.Tests/SanitizerTests.cs index 031a86dbb4..1fbc6b12ce 100644 --- a/test/WebJobs.Script.Tests/SanitizerTests.cs +++ b/test/WebJobs.Script.Tests/SanitizerTests.cs @@ -41,6 +41,9 @@ public class SanitizerTests [InlineData("test,aaa://aaa:aaaaaa1111aa@aaa.aaa.io:1111,test", "test,[Hidden Credential],test")] [InlineData(@"some text abc://abc:aaaaaa1111aa@aaa.abc.io:1111 some text abc://abc:aaaaaa1111aa@aaa.abc.io:1111 text", @"some text [Hidden Credential] some text [Hidden Credential] text")] [InlineData(@"some text abc://abc:aaaaaa1111aa@aaa.abc.io:1111 some text AccountKey=heyyyyyyy text", @"some text [Hidden Credential] some text [Hidden Credential]")] + [InlineData("""{"queueName":"my-q-items","connection":"MyConnection","type":"queueTrigger","name":"qTrigger1","direction":"in"}""", "{\"queueName\":\"my-q-items\",\"connection\":\"MyConnection\",\"type\":\"queueTrigger\",\"name\":\"qTrigger1\",\"direction\":\"in\"}")] + [InlineData("""{"queueName":"my-q-items","connection":"DefaultEndpointsProtocol=https;AccountName=a;AccountKey=b/c==;EndpointSuffix=core.windows.net","type":"queueTrigger","name":"queueTrigger1","direction":"in"}""", "{\"queueName\":\"my-q-items\",\"connection\":\"[Hidden Credential]\",\"type\":\"queueTrigger\",\"name\":\"queueTrigger1\",\"direction\":\"in\"}")] + [InlineData("""{"name":"message","type":"queueTrigger","direction":"In","properties":{"supportsDeferredBinding":"True"},"queueName":"myqueue-items2","connection":"key:setting"}""", """{"name":"message","type":"queueTrigger","direction":"In","properties":{"supportsDeferredBinding":"True"},"queueName":"myqueue-items2","connection":"key:setting"}""")] public void SanitizeString(string input, string expectedOutput) { var sanitized = Sanitizer.Sanitize(input); diff --git a/test/WebJobs.Script.Tests/WorkerFunctionMetadataProviderTests.cs b/test/WebJobs.Script.Tests/WorkerFunctionMetadataProviderTests.cs index 63dd73c554..7fc0737a8e 100644 --- a/test/WebJobs.Script.Tests/WorkerFunctionMetadataProviderTests.cs +++ b/test/WebJobs.Script.Tests/WorkerFunctionMetadataProviderTests.cs @@ -291,5 +291,32 @@ static void AssertFunction(FunctionMetadata function) AssertFunction(function1); AssertFunction(function2); } + + [Fact] + public void ValidateBindings_MasksSensitiveDataInBindings() + { + var functionMetadata = new FunctionMetadata(); + List rawBindings = + [ + """{"type": "queueTrigger","name": "myQueueItem","direction": "in","queueName": "test-input-node","connection": "DefaultEndpointsProtocol=https;AccountName=a;AccountKey=b/c==;EndpointSuffix=core.windows.net"}""", + """{"type": "queue","name": "$return","direction": "out","queueName": "test-output-node","connection": "MyConnection"}""", + ]; + + var function = _workerFunctionMetadataProvider.ValidateBindings(rawBindings, functionMetadata); + Assert.NotNull(function); + Assert.NotNull(function.Bindings); + Assert.Equal(2, function.Bindings.Count); + + // The first binding should have its connection string replaced with "[Hidden Credential]" + var binding1 = function.Bindings[0]; + Assert.NotNull(binding1); + Assert.Equal("[Hidden Credential]", binding1.Connection); + Assert.Equal("[Hidden Credential]", binding1.Raw["connection"]!.ToString()); + + // The second binding should remain unchanged (named connection) + var binding2 = function.Bindings[1]; + Assert.Equal("MyConnection", binding2.Connection); + Assert.Equal("MyConnection", binding2.Raw["connection"]!.ToString()); + } } -} \ No newline at end of file +}