From c47f483fce25b8770d2aa5d46fcb4a12f79d59c6 Mon Sep 17 00:00:00 2001 From: Matt Castelaz Date: Tue, 20 Oct 2026 15:54:21 +0000 Subject: [PATCH 1/4] fix(auth): URL-encode query parameter keys and values in HttpUtils --- .../Auth/Multitenancy/TenantManagerTest.cs | 7 +-- .../Auth/Providers/OidcProviderConfigTest.cs | 6 +-- .../Auth/Providers/SamlProviderConfigTest.cs | 8 ++-- .../Auth/Users/FirebaseUserManagerTest.cs | 5 ++- .../FirebaseAdmin.Tests/Util/HttpUtilsTest.cs | 44 +++++++++++++++++++ FirebaseAdmin/FirebaseAdmin/Util/HttpUtils.cs | 2 +- 6 files changed, 59 insertions(+), 13 deletions(-) create mode 100644 FirebaseAdmin/FirebaseAdmin.Tests/Util/HttpUtilsTest.cs diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Multitenancy/TenantManagerTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Multitenancy/TenantManagerTest.cs index 9ca89f1e..af653241 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Multitenancy/TenantManagerTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Multitenancy/TenantManagerTest.cs @@ -242,7 +242,7 @@ public async Task UpdateTenant(TestConfig config) Assert.Single(handler.Requests); var request = handler.Requests[0]; Assert.Equal(HttpUtils.Patch, request.Method); - var mask = "allowPasswordSignup,displayName,enableEmailLinkSignin"; + var mask = "allowPasswordSignup%2CdisplayName%2CenableEmailLinkSignin"; config.AssertRequest($"tenants/tenant1?updateMask={mask}", request); var body = NewtonsoftJsonSerializer.Instance.Deserialize( @@ -527,7 +527,7 @@ public void ListTenantsOptions(TestConfig config) var customOptions = new ListTenantsOptions() { PageSize = 3, - PageToken = "custom-token", + PageToken = "custom+token=1&2#3", }; var pagedEnumerable = auth.TenantManager.ListTenantsAsync(customOptions); @@ -540,7 +540,8 @@ public void ListTenantsOptions(TestConfig config) Assert.All(tenants, AssertTenant); Assert.Equal(2, handler.Requests.Count); - config.AssertRequest("tenants?pageSize=3&pageToken=custom-token", handler.Requests[0]); + config.AssertRequest( + "tenants?pageSize=3&pageToken=custom%2Btoken%3D1%262%233", handler.Requests[0]); config.AssertRequest("tenants?pageSize=3&pageToken=token", handler.Requests[1]); } diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Providers/OidcProviderConfigTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Providers/OidcProviderConfigTest.cs index 3dd8886f..a0555fec 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Providers/OidcProviderConfigTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Providers/OidcProviderConfigTest.cs @@ -274,7 +274,7 @@ public async Task UpdateConfig(ProviderTestConfig config) Assert.Single(handler.Requests); var request = handler.Requests[0]; Assert.Equal(ProviderTestConfig.PatchMethod, request.Method); - var mask = "clientId,clientSecret,displayName,enabled,issuer,responseType.code,responseType.idToken"; + var mask = "clientId%2CclientSecret%2CdisplayName%2Cenabled%2Cissuer%2CresponseType.code%2CresponseType.idToken"; config.AssertRequest($"oauthIdpConfigs/oidc.provider?updateMask={mask}", request); var body = NewtonsoftJsonSerializer.Instance.Deserialize( @@ -577,7 +577,7 @@ public void ListOidcOptions(ProviderTestConfig config) var customOptions = new ListProviderConfigsOptions() { PageSize = 3, - PageToken = "custom-token", + PageToken = "custom+token=1&2#3", }; var pagedEnumerable = auth.ListOidcProviderConfigsAsync(customOptions); @@ -591,7 +591,7 @@ public void ListOidcOptions(ProviderTestConfig config) Assert.Equal(2, handler.Requests.Count); config.AssertRequest( - "oauthIdpConfigs?pageSize=3&pageToken=custom-token", handler.Requests[0]); + "oauthIdpConfigs?pageSize=3&pageToken=custom%2Btoken%3D1%262%233", handler.Requests[0]); config.AssertRequest( "oauthIdpConfigs?pageSize=3&pageToken=token", handler.Requests[1]); } diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Providers/SamlProviderConfigTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Providers/SamlProviderConfigTest.cs index 7fb9331a..25124761 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Providers/SamlProviderConfigTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Providers/SamlProviderConfigTest.cs @@ -278,8 +278,8 @@ public async Task UpdateConfig(ProviderTestConfig config) this.AssertSamlProviderConfig(provider); var request = Assert.Single(handler.Requests); Assert.Equal(ProviderTestConfig.PatchMethod, request.Method); - var mask = "displayName,enabled,idpConfig.idpCertificates,idpConfig.idpEntityId,idpConfig.ssoUrl," - + "spConfig.callbackUri,spConfig.spEntityId"; + var mask = "displayName%2Cenabled%2CidpConfig.idpCertificates%2CidpConfig.idpEntityId%2CidpConfig.ssoUrl%2C" + + "spConfig.callbackUri%2CspConfig.spEntityId"; config.AssertRequest( $"inboundSamlConfigs/saml.provider?updateMask={mask}", request); @@ -555,7 +555,7 @@ public void ListSamlOptions(ProviderTestConfig config) var customOptions = new ListProviderConfigsOptions() { PageSize = 3, - PageToken = "custom-token", + PageToken = "custom+token=1&2#3", }; var pagedEnumerable = auth.ListSamlProviderConfigsAsync(customOptions); @@ -569,7 +569,7 @@ public void ListSamlOptions(ProviderTestConfig config) Assert.Equal(2, handler.Requests.Count); config.AssertRequest( - "inboundSamlConfigs?pageSize=3&pageToken=custom-token", handler.Requests[0]); + "inboundSamlConfigs?pageSize=3&pageToken=custom%2Btoken%3D1%262%233", handler.Requests[0]); config.AssertRequest( "inboundSamlConfigs?pageSize=3&pageToken=token", handler.Requests[1]); } diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Users/FirebaseUserManagerTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Users/FirebaseUserManagerTest.cs index 198a4e78..d46c1cf3 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Users/FirebaseUserManagerTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Users/FirebaseUserManagerTest.cs @@ -520,7 +520,7 @@ public void ListUsersCustomOptions(TestConfig config) var customOptions = new ListUsersOptions() { PageSize = 3, - PageToken = "custom-token", + PageToken = "custom+token=1&2#3", }; var pagedEnumerable = auth.ListUsersAsync(customOptions); @@ -538,7 +538,8 @@ public void ListUsersCustomOptions(TestConfig config) Assert.Equal(2, handler.Requests.Count); config.AssertRequest( - "accounts:batchGet?maxResults=3&nextPageToken=custom-token", handler.Requests[0]); + "accounts:batchGet?maxResults=3&nextPageToken=custom%2Btoken%3D1%262%233", + handler.Requests[0]); config.AssertRequest( "accounts:batchGet?maxResults=3&nextPageToken=token", handler.Requests[1]); } diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Util/HttpUtilsTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Util/HttpUtilsTest.cs new file mode 100644 index 00000000..f74c3f92 --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Util/HttpUtilsTest.cs @@ -0,0 +1,44 @@ +// Copyright 2026, Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Generic; +using Xunit; + +namespace FirebaseAdmin.Util.Tests +{ + public class HttpUtilsTest + { + [Fact] + public void EncodeQueryParamsNullOrEmpty() + { + Assert.Equal(string.Empty, HttpUtils.EncodeQueryParams(null)); + Assert.Equal(string.Empty, HttpUtils.EncodeQueryParams(new Dictionary())); + } + + [Fact] + public void EncodeQueryParamsEscapesSpecialCharacters() + { + var queryParams = new Dictionary() + { + { "pageToken", "token+1=2&3#4/5" }, + { "pageSize", 100 }, + { "key with spaces", null }, + }; + + Assert.Equal( + "?key%20with%20spaces=&pageSize=100&pageToken=token%2B1%3D2%263%234%2F5", + HttpUtils.EncodeQueryParams(queryParams)); + } + } +} diff --git a/FirebaseAdmin/FirebaseAdmin/Util/HttpUtils.cs b/FirebaseAdmin/FirebaseAdmin/Util/HttpUtils.cs index d946deba..696f02e3 100644 --- a/FirebaseAdmin/FirebaseAdmin/Util/HttpUtils.cs +++ b/FirebaseAdmin/FirebaseAdmin/Util/HttpUtils.cs @@ -36,7 +36,7 @@ internal static string EncodeQueryParams(IDictionary queryParams if (queryParams != null && queryParams.Count > 0) { var list = queryParams.OrderBy(kvp => kvp.Key) - .Select(kvp => $"{kvp.Key}={kvp.Value}"); + .Select(kvp => $"{Uri.EscapeDataString(kvp.Key)}={Uri.EscapeDataString(kvp.Value?.ToString() ?? string.Empty)}"); queryString = "?" + string.Join("&", list); } From ae6c1398108d9fa740e3b707ca7e5d4992c538d2 Mon Sep 17 00:00:00 2001 From: Matt Castelaz Date: Tue, 20 Oct 2026 19:26:23 +0000 Subject: [PATCH 2/4] fix(auth): use CultureInfo.InvariantCulture when encoding query params --- .../FirebaseAdmin.Tests/Util/HttpUtilsTest.cs | 22 +++++++++++++++++++ FirebaseAdmin/FirebaseAdmin/Util/HttpUtils.cs | 3 ++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Util/HttpUtilsTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Util/HttpUtilsTest.cs index f74c3f92..80779548 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Util/HttpUtilsTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Util/HttpUtilsTest.cs @@ -13,6 +13,8 @@ // limitations under the License. using System.Collections.Generic; +using System.Globalization; +using System.Threading; using Xunit; namespace FirebaseAdmin.Util.Tests @@ -40,5 +42,25 @@ public void EncodeQueryParamsEscapesSpecialCharacters() "?key%20with%20spaces=&pageSize=100&pageToken=token%2B1%3D2%263%234%2F5", HttpUtils.EncodeQueryParams(queryParams)); } + + [Fact] + public void EncodeQueryParamsUsesInvariantCulture() + { + var originalCulture = Thread.CurrentThread.CurrentCulture; + try + { + Thread.CurrentThread.CurrentCulture = new CultureInfo("fr-FR"); + var queryParams = new Dictionary() + { + { "ratio", 1.5 }, + }; + + Assert.Equal("?ratio=1.5", HttpUtils.EncodeQueryParams(queryParams)); + } + finally + { + Thread.CurrentThread.CurrentCulture = originalCulture; + } + } } } diff --git a/FirebaseAdmin/FirebaseAdmin/Util/HttpUtils.cs b/FirebaseAdmin/FirebaseAdmin/Util/HttpUtils.cs index 696f02e3..ba14ec25 100644 --- a/FirebaseAdmin/FirebaseAdmin/Util/HttpUtils.cs +++ b/FirebaseAdmin/FirebaseAdmin/Util/HttpUtils.cs @@ -14,6 +14,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Net.Http; using Google.Apis.Json; @@ -36,7 +37,7 @@ internal static string EncodeQueryParams(IDictionary queryParams if (queryParams != null && queryParams.Count > 0) { var list = queryParams.OrderBy(kvp => kvp.Key) - .Select(kvp => $"{Uri.EscapeDataString(kvp.Key)}={Uri.EscapeDataString(kvp.Value?.ToString() ?? string.Empty)}"); + .Select(kvp => $"{Uri.EscapeDataString(kvp.Key)}={Uri.EscapeDataString(Convert.ToString(kvp.Value, CultureInfo.InvariantCulture))}"); queryString = "?" + string.Join("&", list); } From 3ab76efb52f03f432a12024ee07cf348c9a38aa2 Mon Sep 17 00:00:00 2001 From: Matt Castelaz Date: Wed, 21 Oct 2026 02:47:34 +0000 Subject: [PATCH 3/4] test(auth): use standard format test strings --- .../Auth/Multitenancy/TenantManagerTest.cs | 5 ++--- .../Auth/Providers/OidcProviderConfigTest.cs | 4 ++-- .../Auth/Providers/SamlProviderConfigTest.cs | 4 ++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Multitenancy/TenantManagerTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Multitenancy/TenantManagerTest.cs index eadaeb74..c44e440a 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Multitenancy/TenantManagerTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Multitenancy/TenantManagerTest.cs @@ -43,12 +43,11 @@ public class TenantManagerTest "...", "..\n", "tenant\n", - "../other", - "victim.", + "tenant.", "tenant/1", "tenant?foo=bar", "tenant#frag", - "tenant%2e%2e", + "tenant%2f1", "tenant\\1", }; diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Providers/OidcProviderConfigTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Providers/OidcProviderConfigTest.cs index 0b9ea7db..911293e2 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Providers/OidcProviderConfigTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Providers/OidcProviderConfigTest.cs @@ -38,10 +38,10 @@ public class OidcProviderConfigTest "oidc.\n", "oidc.provider\n", "oidc.provider.", - "oidc.provider/../other", + "oidc.provider/1", "oidc.provider?foo=bar", "oidc.provider#frag", - "oidc.provider%2fother", + "oidc.provider%2f1", "oidc.provider&extra=1", }; diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Providers/SamlProviderConfigTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Providers/SamlProviderConfigTest.cs index 3359dfef..341fd2d6 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Providers/SamlProviderConfigTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Providers/SamlProviderConfigTest.cs @@ -35,10 +35,10 @@ public class SamlProviderConfigTest "saml.\n", "saml.provider\n", "saml.provider.", - "saml.provider/../other", + "saml.provider/1", "saml.provider?foo=bar", "saml.provider#frag", - "saml.provider%2fother", + "saml.provider%2f1", "saml.provider&extra=1", }; From 6ddb785a8bb61787545b6a299fc39a43ae506d33 Mon Sep 17 00:00:00 2001 From: Matt Castelaz Date: Wed, 21 Oct 2026 14:30:05 +0000 Subject: [PATCH 4/4] style: wrap query param encoding in HttpUtils --- FirebaseAdmin/FirebaseAdmin/Util/HttpUtils.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/FirebaseAdmin/FirebaseAdmin/Util/HttpUtils.cs b/FirebaseAdmin/FirebaseAdmin/Util/HttpUtils.cs index ba14ec25..9c144b41 100644 --- a/FirebaseAdmin/FirebaseAdmin/Util/HttpUtils.cs +++ b/FirebaseAdmin/FirebaseAdmin/Util/HttpUtils.cs @@ -37,7 +37,13 @@ internal static string EncodeQueryParams(IDictionary queryParams if (queryParams != null && queryParams.Count > 0) { var list = queryParams.OrderBy(kvp => kvp.Key) - .Select(kvp => $"{Uri.EscapeDataString(kvp.Key)}={Uri.EscapeDataString(Convert.ToString(kvp.Value, CultureInfo.InvariantCulture))}"); + .Select(kvp => + { + var key = Uri.EscapeDataString(kvp.Key); + var value = Uri.EscapeDataString( + Convert.ToString(kvp.Value, CultureInfo.InvariantCulture)); + return $"{key}={value}"; + }); queryString = "?" + string.Join("&", list); }