Merge pull request #1864 from rmcrackan/rmcrackan/1862-security

Rmcrackan/1862 security
This commit is contained in:
rmcrackan
2026-06-10 14:31:47 -04:00
committed by GitHub
3 changed files with 97 additions and 18 deletions

View File

@@ -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);
}
/// <summary>
/// Escape a value for use inside single-quoted Newtonsoft JSONPath filter literals.
/// See https://www.newtonsoft.com/json/help/html/QueryJsonSelectTokenEscaped.htm
/// </summary>
internal static string EscapeNewtonsoftJsonPathSingleQuotedLiteral(string value)
=> value.Replace("\\", @"\\").Replace("'", @"\'");
}

View File

@@ -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);
}
}

View File

@@ -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<string>();
[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");
}
}