fix(validation): close Theme 3 — 11 input-validation / unbounded-input findings
Each finding is a focused validation guard or upper bound at a trust boundary.
Highlights:
- Commons-015: EncryptionMetadata ctor now validates Algorithm (AES-256-GCM
only), Kdf (PBKDF2-SHA256 only), Iterations ([100k, 10M]), non-null Salt/IV.
- Transport-004: new BundleUnlockRateLimiter (sliding-window, per-key,
singleton) wired into BundleImporter.LoadAsync; over-budget callers see
BundleUnlockRateLimitedException. Per-bundle 3-strike + per-window cap.
- ESG-022: ExternalSystemClient.InvokeHttpAsync allow-lists the documented
GET/POST/PUT/PATCH/DELETE set (case-insensitive); unknown verbs throw.
- SEL-015: SiteEventLogger queue now bounded (10k cap, DropOldest); dropped
events fault their Task and increment FailedWriteCount so the drop is
observable instead of an unbounded memory growth.
- SEL-017: EventLogQueryService clamps caller-supplied PageSize to a new
MaxQueryPageSize cap (default 500) so int.MaxValue can't OOM the host.
- SEL-020: LogEventAsync rejects severities outside {Info, Warning, Error}
(matches SQLite BINARY-collation query filter).
- InboundAPI-020: ContentType "json" check now case-insensitive
(application/JSON no longer slips through as not-json).
- InboundAPI-024: _knownBadMethods capped at 1000 entries (drops new entries
once full); per-request DB lookup remains the correctness path.
- SR-025: HandleSetStaticAttribute validates the attribute name against the
deployed config; unknown names now return Success=false instead of
leaking orphan override rows into the SQLite store.
- TE-021: MoveTemplateAsync runs the sibling-name-collision check at the
destination, mirroring TemplateFolderService.MoveFolderAsync.
- TE-022: LockEnforcer's once-locked-stays-locked rule now also covers
LockedInDerived (was previously only IsLocked).
New regression tests across 8 test projects (EncryptionMetadata, rate
limiter, ESG client allow-list, SEL bounded channel / PageSize clamp /
severity validation, InboundAPI ContentType + bad-methods cap, SiteRT
unknown-attribute, TemplateEngine MoveTemplate + LockedInDerived).
Build clean; affected suites all green. README regenerated: 93 open (was 104).
Note: a separate manual re-run was needed for the SiteEventLogging hunk
because its initial subagent's source edits never landed on disk despite
reporting success (file-collision-style failure mode).
This commit is contained in:
@@ -67,7 +67,14 @@ public static class EndpointExtensions
|
||||
JsonElement? body = null;
|
||||
try
|
||||
{
|
||||
if (httpContext.Request.ContentLength > 0 || httpContext.Request.ContentType?.Contains("json") == true)
|
||||
// InboundAPI-020: the content-type sniff must be case-insensitive — a
|
||||
// request with `application/JSON` or `Application/Json` is still JSON
|
||||
// and must enter the body-parsing path. The previous case-sensitive
|
||||
// `Contains("json")` silently skipped JSON deserialization for any
|
||||
// capitalised value, leaving `body = null` and surfacing required
|
||||
// parameters as 400 "missing" even though the caller sent a valid body.
|
||||
if (httpContext.Request.ContentLength > 0
|
||||
|| httpContext.Request.ContentType?.Contains("json", StringComparison.OrdinalIgnoreCase) == true)
|
||||
{
|
||||
using var doc = await JsonDocument.ParseAsync(
|
||||
httpContext.Request.Body, cancellationToken: httpContext.RequestAborted);
|
||||
|
||||
@@ -27,8 +27,38 @@ public class InboundScriptExecutor
|
||||
// request for a broken method re-runs the expensive Roslyn compilation — a CPU
|
||||
// amplification vector since the inbound API has no rate limiting. The entry is
|
||||
// cleared whenever the method is (re)compiled via CompileAndRegister.
|
||||
//
|
||||
// InboundAPI-024: bound the cache so a spam attack of unique method names cannot
|
||||
// grow it without bound. Once the cap is reached new bad-method records are
|
||||
// dropped — the cache is just a fast-fail optimisation; the per-request DB
|
||||
// lookup remains the correctness path.
|
||||
private const int KnownBadMethodsCap = 1000;
|
||||
private readonly ConcurrentDictionary<string, byte> _knownBadMethods = new();
|
||||
|
||||
/// <summary>
|
||||
/// InboundAPI-024 diagnostic helper — returns the current size of the
|
||||
/// known-bad-methods cache so tests can assert the cap is honoured. Internal
|
||||
/// so the cache itself stays an implementation detail.
|
||||
/// </summary>
|
||||
internal int KnownBadMethodCount => _knownBadMethods.Count;
|
||||
|
||||
/// <summary>
|
||||
/// InboundAPI-024: records <paramref name="methodName"/> in the known-bad-methods
|
||||
/// cache only if the cache has not reached <see cref="KnownBadMethodsCap"/>.
|
||||
/// Once full, new records are dropped (paying the cheap recompile next time
|
||||
/// rather than leaking memory under a unique-name flood). Existing entries are
|
||||
/// not touched — they remain capped fast-fail records until cleared on a
|
||||
/// successful (re)compile in <see cref="CompileAndRegister"/>.
|
||||
/// </summary>
|
||||
private void TryRecordBadMethod(string methodName)
|
||||
{
|
||||
if (_knownBadMethods.ContainsKey(methodName))
|
||||
return;
|
||||
if (_knownBadMethods.Count >= KnownBadMethodsCap)
|
||||
return;
|
||||
_knownBadMethods.TryAdd(methodName, 0);
|
||||
}
|
||||
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
/// <summary>
|
||||
@@ -73,8 +103,10 @@ public class InboundScriptExecutor
|
||||
if (handler == null)
|
||||
{
|
||||
// InboundAPI-009: record the failure so the lazy-compile path does not
|
||||
// keep recompiling a broken script on every request.
|
||||
_knownBadMethods[method.Name] = 0;
|
||||
// keep recompiling a broken script on every request. InboundAPI-024:
|
||||
// routed through the capped TryRecordBadMethod helper so the cache
|
||||
// cannot grow without bound under a flood of unique method names.
|
||||
TryRecordBadMethod(method.Name);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -230,7 +262,9 @@ public class InboundScriptExecutor
|
||||
if (compiled == null)
|
||||
{
|
||||
// Cache the failure so the next request short-circuits above.
|
||||
_knownBadMethods[method.Name] = 0;
|
||||
// InboundAPI-024: routed through TryRecordBadMethod so the
|
||||
// cache is bounded under a flood of unique method names.
|
||||
TryRecordBadMethod(method.Name);
|
||||
return new InboundScriptResult(false, null, "Script compilation failed for this method");
|
||||
}
|
||||
handler = _scriptHandlers.GetOrAdd(method.Name, compiled);
|
||||
|
||||
Reference in New Issue
Block a user