From 1ad458fe5bce39d50e486984d8ce4d882918df3d Mon Sep 17 00:00:00 2001 From: rmcrackan Date: Wed, 10 Jun 2026 12:12:38 -0400 Subject: [PATCH 1/2] CSPRNG-safe --- Source/AudibleUtilities/Widevine/Cdm.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/AudibleUtilities/Widevine/Cdm.cs b/Source/AudibleUtilities/Widevine/Cdm.cs index faf9def5..04c69f87 100644 --- a/Source/AudibleUtilities/Widevine/Cdm.cs +++ b/Source/AudibleUtilities/Widevine/Cdm.cs @@ -290,7 +290,7 @@ public partial class Cdm private static uint RandomUint() { var bts = new byte[4]; - new Random().NextBytes(bts); + RandomNumberGenerator.Fill(bts); return BitConverter.ToUInt32(bts, 0); } } From 4345971e81bfe39990876d47ee0fb33f68c09c8e Mon Sep 17 00:00:00 2001 From: rmcrackan Date: Wed, 10 Jun 2026 14:29:57 -0400 Subject: [PATCH 2/2] escape string to avoid accidental json injection --- Source/AudibleUtilities/AudibleApiStorage.cs | 26 ++---- .../GetIdentityTokensJsonPathTests.cs | 87 +++++++++++++++++++ 2 files changed, 96 insertions(+), 17 deletions(-) create mode 100644 Source/_Tests/AudibleUtilities.Tests/GetIdentityTokensJsonPathTests.cs diff --git a/Source/AudibleUtilities/AudibleApiStorage.cs b/Source/AudibleUtilities/AudibleApiStorage.cs index c3011a18..ddcee1f1 100644 --- a/Source/AudibleUtilities/AudibleApiStorage.cs +++ b/Source/AudibleUtilities/AudibleApiStorage.cs @@ -62,24 +62,16 @@ public static class AudibleApiStorage => GetIdentityTokensJsonPath(account.AccountId, account.Locale?.Name); public static string GetIdentityTokensJsonPath(string username, string? localeName) { - var usernameSanitized = trimSurroundingQuotes(JsonConvert.ToString(username)); - var localeNameSanitized = trimSurroundingQuotes(JsonConvert.ToString(localeName)); + var usernameEscaped = EscapeNewtonsoftJsonPathSingleQuotedLiteral(username); + var localeNameEscaped = EscapeNewtonsoftJsonPathSingleQuotedLiteral(localeName ?? string.Empty); - return $"$.Accounts[?(@.AccountId == '{usernameSanitized}' && @.IdentityTokens.LocaleName == '{localeNameSanitized}')].IdentityTokens"; + return $"$.Accounts[?(@.AccountId == '{usernameEscaped}' && @.IdentityTokens.LocaleName == '{localeNameEscaped}')].IdentityTokens"; } - private static string trimSurroundingQuotes(string str) - { - // SubString algo is better than .Trim("\"") - // orig string " - // json string "\"" - // Eg: - // => str.Trim("\"") - // output \ - // vs - // => str.Substring(1, str.Length - 2) - // output \" - // also works with surrounding single quotes - return str.Substring(1, str.Length - 2); - } + /// + /// Escape a value for use inside single-quoted Newtonsoft JSONPath filter literals. + /// See https://www.newtonsoft.com/json/help/html/QueryJsonSelectTokenEscaped.htm + /// + internal static string EscapeNewtonsoftJsonPathSingleQuotedLiteral(string value) + => value.Replace("\\", @"\\").Replace("'", @"\'"); } diff --git a/Source/_Tests/AudibleUtilities.Tests/GetIdentityTokensJsonPathTests.cs b/Source/_Tests/AudibleUtilities.Tests/GetIdentityTokensJsonPathTests.cs new file mode 100644 index 00000000..f1487d35 --- /dev/null +++ b/Source/_Tests/AudibleUtilities.Tests/GetIdentityTokensJsonPathTests.cs @@ -0,0 +1,87 @@ +using AssertionHelper; +using AudibleUtilities; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json.Linq; + +namespace AccountsTests; + +[TestClass] +public class GetIdentityTokensJsonPathTests +{ + private const string MarkerProperty = "Marker"; + + private static JObject CreateAccountsJson(params (string accountId, string locale, string marker)[] accounts) + { + var accountsArray = new JArray(); + foreach (var (accountId, locale, marker) in accounts) + { + accountsArray.Add(new JObject + { + ["AccountId"] = accountId, + ["IdentityTokens"] = new JObject + { + ["LocaleName"] = locale, + [MarkerProperty] = marker + } + }); + } + + return new JObject { ["Accounts"] = accountsArray }; + } + + private static string? SelectMarker(JObject root, string jsonPath) + => root.SelectToken(jsonPath)?[MarkerProperty]?.Value(); + + [TestMethod] + public void Normal_email_matches_expected_account() + { + var root = CreateAccountsJson( + ("other@example.com", "us", "target"), + ("someone@example.com", "us", "other")); + + var path = AudibleApiStorage.GetIdentityTokensJsonPath("other@example.com", "us"); + + SelectMarker(root, path).Should().Be("target"); + } + + [TestMethod] + public void Apostrophe_in_account_id_matches_expected_account() + { + var root = CreateAccountsJson(("o'hara@example.com", "us", "target")); + + var path = AudibleApiStorage.GetIdentityTokensJsonPath("o'hara@example.com", "us"); + + SelectMarker(root, path).Should().Be("target"); + } + + [TestMethod] + public void Backslash_in_account_id_matches_expected_account() + { + var root = CreateAccountsJson((@"a\b@c.com", "us", "target")); + + var path = AudibleApiStorage.GetIdentityTokensJsonPath(@"a\b@c.com", "us"); + + SelectMarker(root, path).Should().Be("target"); + } + + [TestMethod] + public void Injection_payload_does_not_match_unrelated_account() + { + var root = CreateAccountsJson( + ("' || @.AccountId == 'evil' || @.AccountId == '", "us", "payload"), + ("evil", "us", "wrong")); + + var path = AudibleApiStorage.GetIdentityTokensJsonPath( + "' || @.AccountId == 'evil' || @.AccountId == '", + "us"); + + SelectMarker(root, path).Should().Be("payload"); + } + + [TestMethod] + public void EscapeNewtonsoftJsonPathSingleQuotedLiteral_escapes_backslash_before_quote() + { + AudibleApiStorage.EscapeNewtonsoftJsonPathSingleQuotedLiteral(@"a\b").Should().Be(@"a\\b"); + AudibleApiStorage.EscapeNewtonsoftJsonPathSingleQuotedLiteral("o'hara").Should().Be(@"o\'hara"); + } +}