Repository navigation
feat(security): WAF-equivalent input validation + GraphQL depth/complexity limit — PR3 (#2270) - #2273
Merged
Merged
Conversation
…exity limit — PR3
Third PR of the origin-abuse-protection series (after PR1 shared rate limiter,
PR2 edge-cache Fast-Purge). Adds a WAF-equivalent input-validation layer on the
anon / agentic write surface plus depth+complexity limiting on the public
GraphQL endpoint. Flag-OFF-first, DB-driven config, env-free, fail-open
everywhere — the validator must never break the surface it protects.
- srv/lib/input-validation/json-shape.js (new): pure, iterative (explicit-stack,
never recursive) depth/keys/array-length walker -> {ok} | {ok:false,reason}.
- srv/lib/input-validation/register.js (new): two-mode express middleware wired
in cds.on('bootstrap') after the rate limiter. 'size' mode = Content-Length cap
only (stream-safe, broad anon POST surface); 'full' mode = size cap + bounded
express.json pre-parse + JSON shape check, scoped to the anon+CSRF-off agentic
JSON-RPC surface (/mcp*, /a2a, /chat/stream). Pre-parse reused by CAP bodyParser4
and graphql-http (no double-consume). 413 payload_too_large / 400 invalid_json /
400 invalid_body_shape. Flag-gated, fail-open, counter metrics.
- srv/lib/graphql-guard.js (new): hand-rolled graphql-js ValidationRule
costLimitRule(maxDepth,maxComplexity) (fragment-resolving, cycle-guarded) +
buildPublicValidationRules — a per-request graphql-http validationRules fn that
appends the cost rule and, in PROD only, NoSchemaIntrospectionCustomRule.
Flag-gated + config-driven, fail-open (returns specifiedRules on off/error).
No new dependency (keeps srv-qa boot + "no new BTP entitlements" safe).
- srv/graphql-config.js: pass validationRules:buildPublicValidationRules to the
/graphql/public GraphQLAdapter.
- srv/lib/runtime-config/input-validation-settings.js (new): ImsConfig-backed
thresholds (~5s cache, globalThis-pinned, never-throws, no env fallback),
mirroring rate-limit-settings.js. DEFAULT_CONFIG: 1MiB body, depth 32, 10k keys,
50k array, gql depth 12 / complexity 1000.
- srv/lib/feature-flags/registry.js: INPUT_VALIDATION_ENABLED db flag
(flag.inputvalidation, default false, dev-only) -> surfaces in Feature Flags UI
and auto-registers with db-flags for isFlagEnabled().
- srv/server.js: registerInputValidation(app) after registerRateLimit(app).
- srv/lib/mcp-{channels,events}-search.js, mcp-kg-tools.js: normalize
caller-controlled limit/depth via the canonical clampLimit() shared validator.
- test/unit/input-validation.test.js (new): json-shape depth/keys/array + ok +
stack-safety, config defaulting, middleware flag-gating/413/400-shape/
size-vs-full/exclude/fail-open, costLimitRule depth+complexity+fragment
rejection, buildPublicValidationRules flag-off/on/prod-introspection/fail-open.
- test/unit/mcp-{channels,events}-search.test.js: updated the limit=0 assertion to
the canonical clampLimit semantics (explicit 0 -> floor 1; only omitted -> default).
srv-qa cp-list: unaffected — none of the new modules are reachable from
content-store.js (verified by reverse-dependency audit); their only importers are
server.js and graphql-config.js, which are not in srv-qa's curated tree.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
PR3 of the origin-abuse-protection series (after #2263 shared rate limiter, #2264 edge-cache Fast-Purge). Closes #2270.
Adds a WAF-equivalent input-validation layer on the anon / agentic write surface, plus depth + complexity limiting (and prod-only introspection block) on the public GraphQL endpoint. Consistent with the series: DB-flag gated, DB-driven config, env-free, fail-open, default OFF.
Layers
srv/lib/input-validation/register.jsmounts a two-mode express middleware incds.on('bootstrap'), right after the rate limiter (floods shed first):sizemode — Content-Length body-size cap only. Stream-safe, so it can't loosen a stricter inline limit (e.g./api/ui-event's 64kb) or interfere with OData parsing. Applied broadly (/content,/build,/graph,/feedback,/homepage,/api,/graphql/public).fullmode — size cap + boundedexpress.jsonpre-parse + JSON depth/keys/array-length shape check. Scoped to the anon + CSRF-off agentic JSON-RPC surface (/mcp*,/a2a,/chat/stream). Pre-parse is reused downstream (CAPbodyParser4early-returns on object body; graphql-http readsreq.body) → no double-consume.413 payload_too_large/400 invalid_json/400 invalid_body_shape.srv/lib/input-validation/json-shape.jsis a pure, iterative (explicit-stack, never recursive) walker./graphql/public—srv/lib/graphql-guard.jsprovides a hand-rolled graphql-jscostLimitRule(maxDepth, maxComplexity)(fragment-resolving, cycle-guarded) and a per-requestbuildPublicValidationRulesfn wired viavalidationRulesingraphql-config.js. Appends the cost rule and, in PROD only,NoSchemaIntrospectionCustomRule. No new dependency (keeps srv-qa boot + "no new BTP entitlements" intact).mcp-channels-search.js,mcp-events-search.js,mcp-kg-tools.jsnow normalize caller-controlledlimit/depthvia the canonicalclampLimit()shared validator.Config & flag
INPUT_VALIDATION_ENABLED(kind:'db', ImsConfigflag.inputvalidation, default false, dev-only) registered infeature-flags/registry.js— surfaces in the Feature Flags UI and auto-registers withdb-flagsforisFlagEnabled().srv/lib/runtime-config/input-validation-settings.js(ImsConfig-backed, ~5s cache, globalThis-pinned, never-throws, no env fallback — blue-green safe). Defaults: 1 MiB body, depth 32, 10k keys, 50k array, gql depth 12 / complexity 1000.next()(orspecifiedRulesfor GraphQL).Existing SELECT-only validator untouched
srv/lib/analytics-sql-validator.cjsstays as its own layer — this PR does not replace it.Tests & verification
test/unit/input-validation.test.js(new, 25 tests): json-shape depth/keys/array + primitives + deep-payload stack-safety; config defaulting; middleware flag-gating / 413 / 400-shape / size-vs-full / exclude / fail-open;costLimitRuledepth + complexity + fragment rejection;buildPublicValidationRulesflag-off/on / prod-introspection / fail-open.test/unit/mcp-{channels,events}-search.test.jslimit=0assertions to the canonicalclampLimitsemantics (explicit0→ floor 1; only omitted → default). These two were the only real test failures introduced; the rest of the unit suite is unchanged (remaining failing files are the pre-existing island/imgly vite transform issues).feature-flags-registry.test.js) passes.content-store.js; their only importers (server.js,graphql-config.js) are not in srv-qa's curated tree.Rollout
Default OFF. Enable in DEV via ImsConfig
flag.inputvalidation, observe, then PROD.