fix(inbound-api): resolve InboundAPI-001/003/005 — concurrent handler cache, constant-time API key compare, script trust-model enforcement
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using ScadaLink.Commons.Entities.InboundApi;
|
||||
using ScadaLink.Commons.Interfaces.Repositories;
|
||||
|
||||
@@ -30,7 +32,14 @@ public class ApiKeyValidator
|
||||
return ApiKeyValidationResult.Unauthorized("Missing X-API-Key header");
|
||||
}
|
||||
|
||||
var apiKey = await _repository.GetApiKeyByValueAsync(apiKeyValue, cancellationToken);
|
||||
// InboundAPI-003: do NOT resolve the key with a secret-equality lookup
|
||||
// (GetApiKeyByValueAsync translates to a SQL "WHERE KeyValue = @secret" early-exit
|
||||
// comparison — a timing side-channel). Fetch all keys and match the secret
|
||||
// in-process with a constant-time comparison so neither match position nor
|
||||
// secret length is observable to a network attacker.
|
||||
var apiKey = FindKeyConstantTime(
|
||||
await _repository.GetAllApiKeysAsync(cancellationToken),
|
||||
apiKeyValue);
|
||||
if (apiKey == null || !apiKey.IsEnabled)
|
||||
{
|
||||
return ApiKeyValidationResult.Unauthorized("Invalid or disabled API key");
|
||||
@@ -53,6 +62,31 @@ public class ApiKeyValidator
|
||||
|
||||
return ApiKeyValidationResult.Valid(apiKey, method);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// InboundAPI-003: Finds the key whose value matches <paramref name="candidate"/>
|
||||
/// using <see cref="CryptographicOperations.FixedTimeEquals"/> over the UTF-8 bytes.
|
||||
/// Every candidate row is compared so that the running time does not depend on the
|
||||
/// match position; length mismatches return false without leaking length timing.
|
||||
/// </summary>
|
||||
private static ApiKey? FindKeyConstantTime(IEnumerable<ApiKey> keys, string candidate)
|
||||
{
|
||||
var candidateBytes = Encoding.UTF8.GetBytes(candidate);
|
||||
ApiKey? match = null;
|
||||
|
||||
foreach (var key in keys)
|
||||
{
|
||||
var keyBytes = Encoding.UTF8.GetBytes(key.KeyValue);
|
||||
if (CryptographicOperations.FixedTimeEquals(candidateBytes, keyBytes))
|
||||
{
|
||||
// Do not break — continuing keeps the loop's timing independent of
|
||||
// where (or whether) a match is found.
|
||||
match = key;
|
||||
}
|
||||
}
|
||||
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user