Skip to content

Security hardening and parser robustness

Who this is for. Someone evaluating TesseraQL, or reviewing it for adoption. It describes the framework’s own posture, not how to secure the application you are building — that is authentication, data scoping, and multi-tenancy.

This page is TesseraQL’s security self-assessment: an OWASP ASVS-shaped map of the framework’s controls to the mechanisms that implement them, and the parser-robustness work that hardens the untrusted-input edge. It is a maintainer document, not a certification — it records what the framework does, where, and what is deliberately out of scope, so a reviewer can check claims against code and a deployment can reason about its own posture.

Two things live here because they belong together: the ASVS control map (what protects each class of risk) and the parser fuzzing that proves the input parsers fail closed. The parsers are the framework’s widest attack surface — 2-way SQL, expressions, YAML, and SCIM filters all turn bytes into structure — so their robustness is assessed with evidence, not assertion.

Its companion is the threat model: where this page maps controls to ASVS chapters, the threat model works the other way — a STRIDE pass over each surface, naming the threats and the control that mitigates each, with the residual risk stated.

Every parser must fail closed on any input: malformed, hostile, or merely huge input yields either a valid parse or a typed, coded rejection the caller can handle — never an uncaught StackOverflowError, OutOfMemoryError, NullPointerException, or a non-terminating loop. A crash or hang on adversarial input is a denial-of-service vector; on the runtime-reachable parsers it is one an authenticated client could trigger.

Exposure differs by parser, and the assessment is honest about it:

Parser Where it parses Input trust
2-way SQL (Sql2WayParser) app SQL files, at build and boot author-controlled; semi-trusted for a shared .tqlapp the admission gate parses
Expressions (ExpressionParser) route conditions, binds, validate: rules author-controlled (compiled once; values bind at runtime, the tree does not re-parse)
YAML (SimpleYamlParser over Jackson/SnakeYAML) app documents at boot and request bodies at the Studio editor endpoints author files are trusted; Studio request bodies are runtime, authenticated, client-controlled
SCIM filter (ScimFilter, ScimGroupPatch) the ?filter= / PATCH path from a provisioning client runtime, authenticated, client-controlled

Adversarial probes against each parser found two real defects and confirmed the rest safe:

  • 2-way SQL: deep directive nesting overflows the stack. A template nesting /*%if …*/ / /*%for …*/ / /*%scope …*/ past roughly 1500 levels raises a raw StackOverflowError from the recursive-descent block parser, rather than a clean parse error. Author-controlled in the common case, but a shared app could crash the admission linter this way.
  • Expressions: deep nesting overflows the stack. ((((…)))) grouping or a !!!!… unary chain past roughly the same depth overflows ExpressionParser. Same class, same fix.
  • Expressions: a number-shaped token the JDK rejects leaks a raw exception. The fuzz harness (not the hand probes) surfaced this: the lexer accepts number tokens the JDK’s Long.parseLong/Double.parseDouble still reject — an integer past long’s range, a malformed exponent — so ExpressionParser raised an uncoded NumberFormatException instead of a parse error. Now wrapped as a coded rejection; the find is exactly the off-contract-exception class the harness exists to catch.
  • YAML resource limits are the library’s defaults, and the error contract is inconsistent. Jackson and SnakeYAML do cap nesting (1000) and document size (~3 MB code points) by default, so an alias bomb or deep document is rejected. The framework sets no explicit bound of its own, and the rejection surfaces differently by entry point: the file parse methods wrap it as a raw, uncoded UncheckedIOException, while the string methods the Studio endpoints call return a coded TQL-YAML-1001. Same malformed input, two contracts.
  • SCIM filters are safe. The filter and group-patch parsers are single anchored regexes (eq-only), not recursive descent; deep nesting simply fails the match, and ReDoS probes (millions of quotes, spaces, and value characters) all terminate promptly. No change needed — the fuzz harness locks the invariant in.
  • Depth guards. Sql2WayParser and ExpressionParser count nesting depth and raise a coded TqlException when it exceeds a generous bound (far above any real template, far below the overflow threshold), converting a fatal StackOverflowError into an ordinary parse rejection.
  • Explicit YAML constraints and one error contract. The YAML mapper is configured with explicit StreamReadConstraints (nesting, document, and name/string length) rather than relying on library defaults that a dependency change could move, and the file parse methods harmonize onto the same coded TQL-YAML-1001 the string methods already return.
  • The fuzz harness proves it. A deterministic generative harness (below) drives each parser with structure-aware and mutated inputs on every build, asserting the fail-closed invariant — so a regression that reintroduces a crash fails the build, not production.

TesseraQL fuzzes with deterministic, generative property tests that run in ordinary CI, not a coverage-guided native fuzzer. A seeded PRNG drives structure-aware generators (each parser’s own alphabet of directives, operators, and delimiters) plus mutation of a seed corpus of valid inputs; a fixed iteration budget runs on every build. The oracle is the invariant above: the only Throwable a parser may raise is its declared, coded exception type; anything else — StackOverflowError, OutOfMemory, NullPointerException, StringIndexOutOfBounds, a raw library exception, or a timeout — is a failure. Each specific finding above also gets a fixed regression test, so the exact defect can never return even if the generator’s seed moves.

This is a deliberate choice. A coverage-guided fuzzer (Jazzer/libFuzzer) explores deeper but adds a native, non-deterministic dependency on the CI path — against the framework’s JDK-only, reproducible-build grain. The harness is written so its per-parser entry points could be wrapped by a gated Jazzer campaign later (the same way the vendor-dialect portability suites run out of the per-change path), the day the depth of exploration is worth the dependency. Until then, deterministic generation catches the whole class of crash bug — it already found the two above — while staying green and reproducible.

There is no component registry to guard. Application YAML never carries a raw endpoint URI — recipes construct every step — and since the framework stopped resolving anything by name off the classpath (docs/camel-removal.md, design record), a dependency upgrade or a plugin JAR can no longer arm an exec or groovy endpoint by being present.

That closed a control rather than weakening one. The guard that used to refuse such registrations at boot — and the lint that caught an attempt to re-allow one — was deleted with the mechanism it defended, error codes included; the threat model it was written against is kept as a record in docs/component-guard.md. What the classpath can still reach is bounded by the dependencies the runtime declares — 153 jars, enforcer-pinned — and by the egress allow-list below, which governs the outbound calls a step can actually make.

A self-assessment against OWASP ASVS Levels 1–2, by chapter. Each row names the control class, its status (met / partial / gap / n/a), and where it lives; the per-requirement detail is filled in the assessment below.

ASVS chapter TesseraQL surface Where
V1 Architecture module boundaries, SPI seams, deny-by-default posture this page
V2 Authentication bearer/api-key/mTLS/browser + SAML/OIDC; PBKDF2; TOTP authentication, credential-lifecycle, saml
V3 Session management server session store, per-session CSRF, fixation/rotation account
V4 Access control deny-by-default PolicyEngine, field/row scoping, tenancy data-scoping, multi-tenancy
V5 Validation / encoding / injection 2-way SQL bind discipline, embedded-var enum gate, Thymeleaf escaping, CSP, parser robustness two-way-sql, declarative-validation, this page
V6 Cryptography JDK-only crypto, secret references, no keys in source authentication
V7 Errors / logging TQL-* taxonomy, route audit with masked fields, MDC trace id reference-error-codes
V8 Data protection field masking/classification, attachment scan gate attachments
V9 Communications TLS termination stance, mTLS deployment
V11 Business logic / rate per-node and cluster rate limits, workflow guards deployment
V12 Files / resources attachment scanning, blob egress allow-list, the DuckDB fence attachments, duckdb
V13 API / web service SCIM, MCP, REST recipes; CSRF for browser auth authentication
V14 Configuration deny-by-default egress allow-list, admission profile, secret hygiene admission

The requirement-level view, by chapter. Status is met (a control implements it), partial (implemented with a caveat or a deployment dependency), gap (a known shortfall, carried into follow-up work), or n/a (outside the framework’s responsibility). Citations name the class or the doc; this is a self-assessment, so a reviewer can check each claim against the source.

Requirement Status Evidence
Deny-by-default security posture met routes require an auth mode and a policy unless auth: public; egress, uploads, and admission all default closed
Trust boundaries and component isolation met module boundaries hold (tesseraql-core dependency-free); heavy/optional capabilities sit behind SPIs (FileCodec, BlobStore, SecretResolver, PasswordVerifier); the DuckDB engine runs behind a per-connection fence
A documented threat model gap this page is the first hardening artifact; a full threat-model refresh is follow-up work
Requirement Status Evidence
Passwords stored with an approved KDF met Pbkdf2PasswordEncoder — PBKDF2 (100k iterations, 256-bit key, 16-byte random salt); Argon2id/bcrypt plug in behind PasswordVerifier
Credentials verified in constant time met ApiKeyAuthenticator SHA-256-hashes the presented key and constant-time compares; the raw key is never stored or logged
Token/signature verification is sound met JwtAuthenticator (JDK-only HS256/RS256 with JWKS) rejects algorithm confusion by design; SAML pins the IdP key with XXE off and a DB replay guard; OIDC is authorization-code + PKCE
A second factor is available met RFC 6238 TOTP (Totp) with recovery codes and a last-used-step replay block
Anti-enumeration on credential flows met reset/invite/login return neutral responses end to end (credential-lifecycle)
Memory-hard KDF at L2 partial PBKDF2 is the default (ASVS-acceptable); Argon2id is available but not the shipped default
Requirement Status Evidence
Server-side session with an unpredictable id met SessionStore (tesseraql_sid), in-memory or shared JDBC store
No session fixation met BrowserAuthenticator.create() mints a fresh id at login; no pre-auth session is adopted
Logout and “sign out others” invalidate server-side met invalidate() / invalidateOthersFor(); a consumed password reset drops every session of the subject
Sessions ended on a credential change met a self-service password change ends every session of the subject and returns the caller to the login page — an attacker’s parallel session is evicted
Session id re-issued in place on other elevations met response.session.rotate (docs/session-rotation.md) re-issues the cookie — fresh id and CSRF token, old id invalidated first — after a successful elevation; the account app’s TOTP enrollment confirm declares it
Requirement Status Evidence
Deny-by-default authorization met PolicyEngine.authorize() — no principal → TQL-SEC-4011 (401), undefined/unsatisfied policy → TQL-SEC-4031 (403)
Field- and row-level enforcement met FieldPolicyApplier (role-conditional column masking + row unmaskWhen); the /*%scope*/ data-scoping directive fails closed (TQL-SQL-2106/2107)
Multi-tenant isolation met shared-schema / schema-per-tenant / db-per-tenant, tenant resolved per request and enforced in SQL; TQL-TENANT lint
Write-scope guard met a lint (TQL-SEC-4100, warning) flags an UPDATE/DELETE on a scope-governed table that carries no /*%scope … */ predicate — defense-in-depth over the scoping mechanism the author already uses

V5 Validation, sanitization, encoding, injection

Section titled “V5 Validation, sanitization, encoding, injection”
Requirement Status Evidence
SQL injection prevented structurally met 2-way SQL bind values are always parameterized; the only text-interpolating path (embedded {var}) is enum-gated for request inputs (TQL-SQL-2109)
Output encoding / XSS met HTML renders through Thymeleaf context-aware auto-escaping; every HTML page must carry a CSP (TQL-ADM-4704)
Declared input validation met declarative input constraints + validate: rules (declarative validation)
Parsers fail closed on hostile input met depth guards + explicit YAML constraints + the deterministic fuzz harness (this page) — three defects found and fixed
Requirement Status Evidence
Approved primitives, no home-rolled crypto met JDK-only crypto throughout (JCA); no custom cipher/MAC construction
Keys and secrets out of source met SecretResolver SPI (${secret.*}, env/file/vault); resolved lazily per use, never written to logs or generated artifacts
Randomness for security tokens met SecureRandom for salts, session ids, PKCE verifiers, reset tokens
Requirement Status Evidence
Errors don’t leak internals met ErrorResponseRenderer returns generic bodies; SAML/OIDC failures are generic 401/400, contents never echoed
A structured, greppable error taxonomy met 336 TQL-<DOMAIN>-<NNNN> codes across 33 domains, generated into the reference and drift-guarded
Audit trail without sensitive data met tql_route_audit records who/what/when over declared inputs only, and excludes any field marked mask:/classification: wholesale
Log correlation met MDC bridges the OTel traceId/spanId into logs
Requirement Status Evidence
Sensitive fields masked/classified met field masking + classification: drive both response shaping and audit exclusion
Uploads gated before release met the attachment download gate refuses any non-clean object, including pending in async scan mode — fail-closed
At-rest encryption n/a a database/storage-layer concern; the framework stores blobs through a pluggable store and never mandates cleartext
Requirement Status Evidence
TLS in transit met TLS termination is the deployment edge’s responsibility (reverse proxy / ingress), stated in the operator guide (deployment, “Transport security”): HTTPS forwarded to the runtime, HSTS set at the edge, mTLS client-cert forwarding, and outbound cert verification kept on
Requirement Status Evidence
Rate limiting met per-node token bucket (RateLimiter) and a cluster-wide leased budget (ClusterRateLimiter, TQL-RATE-4291) that degrades to per-node
Anti-automation on credential surfaces met keyed failures-only throttle (CredentialThrottle, TQL-RATE-4292, docs/credential-throttle.md): per-login (before existence checks — no enumeration oracle) + per-address budgets over login/reset/invite and the SSO callbacks; on by default, never a lockout
Sequential/idempotent business flows met the transactional command engine (single-connection commit) + at-least-once messaging with idempotency keys; workflow transition guards
Requirement Status Evidence
Untrusted files handled safely met attachment scanning (fail-closed gate); the DuckDB fence confines engine file access to declared roots with a traversal-proof path-placeholder discipline
Object-store egress bounded met BlobStore S3 access is deny-by-default via allowedBuckets; the DuckDB remote tier uses prefix-scoped secrets
Resource-exhaustion parsing bounded met the parser depth guards and explicit YAML read constraints (this page)
Requirement Status Evidence
Authentication on every service endpoint met SCIM and MCP routes gate on bearer + a policy; REST recipes carry the same security: contract
CSRF protection for browser sessions met CsrfValidator — state-changing auth: browser requests must present X-CSRF-Token matching the per-session token (TQL-SEC-4032)
Untrusted query input parsed safely met the SCIM ?filter= parser is anchored and fuzz-verified ReDoS-safe (this page)
Requirement Status Evidence
Deny-by-default egress met tesseraql.http.outbound.allowedHosts (and the copilot endpoint, TQL-SEC-4085); a bare * is rejected at admission (TQL-ADM-4703)
A hardening gate for shared apps met the admission profile enforces declarative-only, defined policies, bounded egress, and CSP before publish
Secret hygiene in development met SECURITY.md — never commit secrets; do not bind-mount host credential directories into the Dev Container
A published security policy surfaced to users met SECURITY.md states the supported-versions policy (latest-release-only pre-1.0, tightening to a support window at 1.0), private vulnerability reporting, and dev-secret hygiene; linked from this page

The honest shortfalls, none blocking for the current pre-1.0 posture:

  1. A full threat-model refresh (V1) — this assessment is the first artifact.
  2. Session id rotation in place on a non-credential elevation (V3) — a fresh id is minted at login (fixation), and both a password reset and a self-service password change now end the subject’s sessions; the residual is re-issuing the current id in place on a non-credential elevation (e.g. MFA enrollment), which the service layer cannot do without cookie plumbing, so it signs out instead.
  3. Argon2id as the shipped default KDF (V2) — a deliberate choice, not a shortfall: PBKDF2 (100k iterations) is an ASVS-approved KDF and keeps the default JDK-only, where Argon2id would require a non-JDK dependency; Argon2id remains available behind PasswordVerifier for deployments that add it.
  4. Write-scope guard (V4) — now implemented: TQL-SEC-4100 (warning) flags an UPDATE/DELETE on a table the app scopes elsewhere that omits its own /*%scope … */ predicate. The scoping mechanism always confined writes when used (data scoping); the guard is the defense-in-depth check that catches a forgotten one.

Two shortfalls recorded here were closed in this pass: the edge TLS/HSTS operator expectations are now stated in deployment, and SECURITY.md carries a proper supported-versions policy and is linked above. A full threat-model refresh remains the substantive follow-up.

Deliberately out of scope (documented, not implied)

Section titled “Deliberately out of scope (documented, not implied)”
  • A certified audit. This is a maintainer self-assessment; it makes no compliance claim.
  • Coverage-guided fuzzing on the CI path — the harness is ready for a gated campaign when the dependency is worth it.
  • A full threat-model refresh and the SECURITY.md supported-versions/LTS statement — tracked as follow-up hardening work, not part of this assessment.