-
Notifications
You must be signed in to change notification settings - Fork 151
Audit roadmap
Full audit of LDH's codebase and dependencies, evaluation of the product feature set and UX, and a prioritized future-direction brainstorm — standalone and combined with the sibling stack (../Web-Algebra, ../REST-VKG, ../AutoGraph). Findings were gathered by parallel codebase explorations, spot-verified against pom.xml, Application.java, URLValidator.java, Item.java, and layout.xsl, and cross-referenced against the project wiki — which already documents a Roadmap, a Radically Open Security penetration-test report, and detailed specs for graph versioning, the v6 XHTML+RDFa format, and a PATCH refactor. This document reconciles the independent audit with those existing plans rather than duplicating them.
Already decided / in progress (excluded from priorities below):
-
com.atomgraph:clientupgrade to 5.x — done. (Removes the former top P0 and the jakarta.json/slf4j exclusion debt; unblocks upstreaming generic XSLT to Web-Client, item P2.6.)
Modern foundation: Java 21, Jena 6.1.0, Jersey 3.1.11 (Jakarta), Tomcat 10.1.52 base image, non-root container, Docker secrets, healthchecks. v5.5.4 shows active hardening (HTTP client timeouts, SignUp response-leak fixes), and several 2025 pen-test findings are already remediated in-code (below).
The wiki's Penetration test report (Radically Open Security, Oct 2025, target v5.0.23) found 6 issues. Reconciled against the current v5.5.4 source:
| ID | Finding | Sev | Status in v5.5.4 (evidence) |
|---|---|---|---|
| LNK-011 | Stored XSS via file-upload MIME type | High |
Fixed. Item.java:188,199 sets Content-Security-Policy: default-src 'none'; sandbox (explicit // LNK-011 fix) |
| LNK-005 | DoS via XML entity expansion (Billion Laughs) | High |
Likely fixed on the RDF path — BillionLaughsTest exists in Core's io. But XSLTMasterUpdater's DocumentBuilder is a separate, un-hardened parser → P0.1 below
|
| LNK-003 | SSRF via ?uri= fetching arbitrary URLs |
Elevated |
Partially fixed. URLValidator blocks link-local/site-local; DNS-rebinding residual remains → P0.2
|
| LNK-009 | SSRF → admin Fuseki (fuseki-admin:3030), ACL bypass |
Elevated |
Direct path closed (site-local now blocked) but loopback is deliberately NOT blocked (URLValidator.java:58,81) → P0.2
|
| LNK-004 | SSRF via unverified On-Behalf-Of header |
Low |
Fixed. URLValidator.validate() now called on the On-Behalf-Of URI in WebIDFilter
|
| LNK-002 | SSRF in admin /transform (dct:source, spin:query) |
Low |
Open. Transform.java/Add.java/Generate.java still fetch server-side → closed by the PATCH refactor (P1.5) |
Dependencies
-
java-jwt3.19.4 (2021, EOL major line) sits on the authentication path (Google OAuth2, ORCID OIDC). - Guava 31.1-jre (2021; CVE-2023-2976 in
FileBackedOutputStream, likely unused — bump is free). - No high-CVE-rate libs in LDH itself; NF-001 in the pen test (Fuseki CVE-2023-32200) is mitigated by the Graal-less Java 21 install.
Architecture debt
-
Application.javais a 2,389-line god class with a ~36-param constructor (config parsing, HTTP client factory, SSL/keystore assembly, threeExpiringMapcaches, XSLT setup). This is why it has zero unit tests. - Heavyweights:
DocumentHierarchyGraphStoreImpl(1,107),LoginBase(689),SignUp(592),AuthorizationFilter(450). -
NoopHostnameVerifieron internal clients (Application.java~1629/~1736) — sound only while the cert-pinnedSSLContextinvariant holds; nothing guards it. - WebID model cache TTL hardcoded to 1 day (
Application.java:297, literalTO-DO) — a revoked WebID stays valid up to 24h. -
evalinplatform/entrypoint.shover env-derived strings.
Testing
- ~6 unit test files. The security-critical 20% (auth filters, JWT,
AuthorizationFilter) has ~0% unit coverage; the ~193 shell-basedhttp-tests/give solid integration coverage but slow feedback and weak negative-case reach.
Uniquely coherent data-driven model (document = named graph; apps/UI/ontologies/ACLs all RDF; SPIN constructors + SHACL forms; multi-dataspace via subdomains; Linked Data proxy with client-side rendering). Rich view modes (List/Table/Grid/Chart/Map/3D Graph), CSV/RDF imports, HTML+JSON-LD scraping, content-addressed uploads, saved SPARQL queries, package system, WebID + OAuth2 + ORCID auth, 24 CLI scripts. The shared XSLT 3.0 SSR/CSR pipeline via Saxon-JS is genuinely differentiated.
-
Bootstrap 2.3.2 (2013) — biggest UX liability. Viewport meta is present (
layout.xsl:296) but the fixed grid means no real mobile experience. (The v6 track dissolves this — see P2.1.) - No versioning/history/undo/audit — one root cause, three user-facing gaps (wiki has a full spec — P2.2).
- Search is label/regex-only — no full-text over document body (wiki Roadmap flags this too — P1.4).
- No batch operations, provenance UI, event/notification layer, or machine-readable agent capability description.
- 47.5k lines of XSLT in 39 files (
view.xsl2,327;modal.xsl1,861;form.xsl1,746) — some generic, belongs upstream in Web-Client.
The independent audit converged on much of the existing wiki Roadmap (validating the direction): full-text search, inline RDF editing, HTML+RDFa editor, client-side proxy removal, package support, document versioning (Memento), multi-language UI, map/graph refinements, XSLT 3.0 package refactoring. Items the audit adds emphasis to that are under-weighted in the wiki Roadmap: agent-grade auth, machine-readable self-description for LLM harnesses, Application.java decomposition, and the pen-test remediation backlog.
Every major agent harness locks up agent memory in opaque, non-portable stores. LDH + Web-Algebra + REST-VKG is the only stack where all four hold at once, composed on W3C standards (unforgeable by lock-in vendors):
- Open, queryable, ACL'd agent memory (LDH — typed RDF under WebID/WAC, portable across models/harnesses);
- Declarative, inspectable agent plans (Web-Algebra JSON ops — auditable before/after);
- Live user data queryable in place, zero-copy (REST-VKG — Gmail/Calendar/GitHub/Workday as virtual graphs);
- A real human UI over the same substrate (LDH rendering — human-in-the-loop is not a bolt-on).
Solid has (1) without (3)/(4); agent frameworks have neither (1) nor (4); enterprise platforms have (3)+(4) without openness. Strongest tailwind: EU digital-sovereignty policy (EHDS, GAIA-X, Data Act).
| # | Direction | Code fit | Minimal compelling demo |
|---|---|---|---|
| D3 |
Agentic BI / instant portals — NL → live dashboard via ldh-GeneratePortal, ldh-AddResultSetChart, ldh-AddView over any SPARQL endpoint or VKG |
Strong, underexploited | "Build a portal over our Workday org data" → live LDH dashboard in <1 min, no ETL |
| D2 | MCP-native knowledge platform — LDH as persistent, shared, versioned memory for any harness via Web-Algebra's MCP server | Strong — MCP server + LDH ops exist | Claude told "remember X" → typed ACL'd document; a different harness reads it back |
| D1 | Personal Dataspace / open agent memory — AutoGraph productized | Working PoC today |
docker compose up → OAuth once → morning brief in Signal, backed by browsable LDH dataspace |
| D4 | Enterprise VKG platform — REST-VKG + LDH as governance/catalog UI | Good on read; gaps: per-user OAuth brokering, write-VKG, audit | Federated Workday×Gmail×GitHub query with per-graph ACLs |
| D5 | FAIR research data platform | Adequate; slow grant-driven sales | Keep as an application story of D1's features |
Recommended sequencing: D3 → D2 → D1. D3 is the gasp-demo this quarter with near-zero new code; D2 rides the MCP wave and makes the stack discoverable to every agent developer; D1 is the destination product but needs versioning/provenance and onboarding first. D4 monetizes what D2/D3 prove.
The former top P0 (client 5.x upgrade) is done. Remaining P0 is the open pen-test remediation.
| # | Action | Effort | Status |
|---|---|---|---|
| P0.1 |
XXE-harden XML parsing: new SecureXML helper; XSLTMasterUpdater parses with DTDs/external entities disabled; ldh:send-request (SendHTTPRequest) parses external responses with secure processing + external entities off. Closes residual of LNK-005
|
S | ✅ done |
| P0.2 |
Block loopback + wildcard in URLValidator, check all resolved addresses (DNS-rebinding window narrowed). Closes residual of LNK-003/LNK-009; URLValidatorTest extended; ALLOW_INTERNAL_URLS kept as dev escape hatch |
S–M | ✅ done |
| P0.3 |
Upgrade java-jwt 3.19.4 → 4.5.2; fixed the one breaking call (IDTokenFilterBase TokenExpiredException now takes an Instant); added JWKS-based JWTVerifierTest (6 cases) |
S–M | ✅ done |
| P0.4 | Remove eval from entrypoint.sh
|
S | ⏸ deferred — needs container-startup testing; low/theoretical severity (not an ROS finding); the eval'd *_PARAM strings are script-constructed, not attacker-supplied. Guava → 33.6.0 already shipped in 5.6.0 |
| P0.5 |
Document the NoopHostnameVerifier pinned-truststore invariant at both socket-factory sites |
S | ✅ done (comment). Truststore-pinning unit test deferred to P1.1 — the god-class blocks a clean test seam |
| — | Request a retest from Radically Open Security once this branch ships (report recommends it). | — | pending release |
| # | Action | Effort |
|---|---|---|
| P1.1 |
Decompose Application.java: extract ApplicationConfig, HttpClientFactory, CacheRegistry, XSLT setup; wire via Jersey AbstractBinder. Stage it (caches + client factory first) |
L (staged) |
| P1.2 |
Unit tests for auth + proxy decision logic (AuthorizationFilter, WebID validation, JWT, proxy edge cases) |
M |
| P1.3 |
Configurable cache TTLs (Application.java:297) following the v5.5.4 CLIENT_* env-var pattern |
S |
| P1.4 |
Real full-text search behind the existing search modal: enable jena-text (Lucene) in the Fuseki assembler; index labels + ldh:content. Internal implementation detail — public SPARQL contract unchanged. (Wiki Roadmap item; supersedes the regex search) |
M |
| P1.5 |
Replace /add, /transform, /generate with client-orchestrated graph PATCH (per the wiki proposal). Double win: eliminates the LNK-002 server-side SSRF surface and collapses 3 endpoints into standard per-graph PATCH — squarely on the declarative/standards values. Bounded by SaxonJS XML-only parsing (RDF/XML or format-converting proxy); ship in the wiki's 4 steps (/add → /transform → /generate → cleanup) |
M |
| P1.6 |
AGENTS.md / machine-readable self-description for LDH (conventions, PATCH write discipline, read-only SPARQL, auth), mirroring REST-VKG's per-service pattern. Cheapest multiplier for the whole agent story |
S–M |
| P1.7 |
Renovate/Dependabot + dependency-check-maven; nginx limit_req on /sparql, auth, proxy paths (declarative, zero app code) |
S |
| P1.8 | Mobile triage, not rewrite: responsive override CSS for navbar/modals/forms. Defer Bootstrap 2→5 — the v6 track replaces the rendering layer anyway; don't pay twice | S–M |
| P1.9 | Batch operations UI: multi-select in container List/Table → batch delete/move/type-assign as client-composed PATCH/DELETE. Directly relieves AutoGraph's curate-hundreds-of-imports pain | M |
| # | Action | Effort |
|---|---|---|
| P2.1 |
v6: XHTML+RDFa as canonical format (per the wiki spec — ldh:content rdf:XMLLiteral, RDFa→SPARQL-UPDATE index, CSS from @typeof via ldh:FrameworkClass mode, xsl/ldh/ replacing bs2:). Ship in the spec's Phases A–E (A: uncomment @typeof/add @property, non-breaking → B: XHTMLRDFaReader for application/xhtml+xml → C: ldh:FrameworkClass client-side → D: RDFa-Editor integration → E: framework-agnostic xsl/ldh/, remove Bootstrap + v5 migration XSLT). Fold in the ../RDFa-Editor sibling as the editing surface. Gate with http-tests asserting round-trip graph equivalence. Open items to decide (spec omits): transitional back-compat, IXSL client-side perf, home for non-RDFa-expressible system metadata |
L (multi-quarter) |
| P2.2 |
Git-backed graph versioning + Memento (per the wiki Implementation Plan): VersioningFilter (async, post-response) → GraphVersioningService (fixed 4-thread pool, N-Triples serialization, per-path SHA conflict detection) → lightweight Jersey GitHubClient (4 endpoints, no new deps) → Memento TimeGate/TimeMap. Config via doap:GitRepository in system.trig; token from GITHUB_TOKEN env, never RDF. Closes history + undo + audit gaps. Wiki effort: ~2 wks MVP, 6–7 wks full. Pairs naturally with P2.1 (git-versioning XHTML) |
L |
| P2.3 | PROV-O sidecar emission on writes (server-side response filter → per-dataspace provenance graph; agent WebID, timestamp, method, git SHA from P2.2). Server-side = every client gets provenance free. Per-dataspace opt-in via settings | M |
| P2.4 | History/provenance UI: per-document History tab (Memento timemap → diff), per-block "who/when/from-what". After P2.2/P2.3 | M |
| P2.5 |
Vector retrieval as a SPARQL SERVICE: finish ../RdfVectorIndex as a standalone endpoint federated via SERVICE — completes hybrid retrieval (structural + text:query + vector) without touching LDH's endpoint contract |
M–L |
| P2.6 | Upstream generic XSLT to Web-Client 5.x (now unblocked by the client upgrade); keep LDH's XSLT strictly domain-specific | L (incremental) |
| # | Action | Effort |
|---|---|---|
| I1 | One-artifact composed stack: top-level compose/Helm bundling LDH + REST-VKG + Web-Algebra (HTTP/MCP) + RdfVectorIndex, shared network/secrets. Align versions first (AutoGraph pins LDH 5.5.3) | M |
| I2 | Agent-grade auth in LDH: scoped bearer tokens (Solid-OIDC-style) mapped to agent WebIDs with WAC intact + polished WebID delegation. Every P3 direction hits this wall. After P1.1/P1.2 | L |
| I3 |
Workflow registry as graph entities: Web-Algebra workflows stored as LDH documents (schema:Action + payload), each run emitting prov:Activity (needs P2.3) |
M |
| I4 |
Event/trigger layer, hypermedia-style: Linked Data Notifications / WebSub on LDH writes — enables event-driven Web-Algebra schedules and keeps the vector index fresh. Also fixes federation cache coherence (Varnish ?uri= TTL-only staleness) and cross-instance versioning notification (Federation evaluation C4/C5). The only genuinely new server surface; design once, four consumers |
M–L |
| I5 | Write-VKG (inverse GRDDL) in REST-VKG: XSLT 3.0 JSON-output transforms so agents write to Gmail/Calendar through the same RDF interface they read — closes the open-memory loop | L (per service) |
Items not already covered above (P1.5 = uniformity convergence; I2 = identity groundwork; I4 = change propagation):
| # | Action | Effort |
|---|---|---|
| F1 |
Automate acl:delegates enrollment: a flow that adds a node's secretary to the user's WebID profile — itself a PATCH on the profile document via the uniform interface, so the trust topology bootstraps through the same write path (profile-settings affordance in XSLT; no new server surface, no new vocabulary). Extends I2. Separately: decide cross-operator ID-token delegation — deprecate by gating the IDTokenDelegationFilter registration (ProxyRequestFilter.java:224-226) per the code's own warning (IDTokenDelegationFilter.java:30), or adopt RFC 8693 token exchange (with I2) |
M |
| F2 |
Remote-edit UX — implemented (client-only). Investigation showed render-time gating already consumed forwarded remote acl:mode Link headers via the window-global LinkedDataHub.acl-modes; the real defect was that global going stale on fetch-less tab switches (ldh:TabSwitch) — wrong edit affordances when panes face different nodes. Fix: modes are per-pane state like data-base/data-endpoint — bs2:TabBody stamps data-acl-modes (document.xsl), SSR pane stamped from server-side acl:mode() (layout.xsl), create/reuse paths stamp from the response Link headers, and ldh:ActivateTab syncs the window flags from the activated pane via the new ldh:SetAclModes template (client.xsl). acl:mode() and all consumers unchanged. Write transport needed no work: proxy forwards method + entity (ProxyRequestFilter.java:238-241) and every client write path is ldh:href()-wrapped (form.xsl:226,252,728,1515) |
done |
| F3 |
VoID/DCAT dataspace self-description so dataset boundaries are discoverable in-band — required for cross-node search, not for follow-your-nose browsing. Derivable from dataspaces.trig + service metadata (seeded documents, or a small resource class); advertise via the registered describedby link relation |
S–M |
| F4 | Topology decision (Federation evaluation C8): hybrid proxy — client-side loading for public reads, proxy for authenticated traffic — relieves the proxy chokepoint without reopening the credential problem the no-CORS design solves. Code shape: CORS response filter emitting headers only for publicly readable responses + a direct-fetch-with-proxy-fallback branch in the client loading path. Refines the wiki Roadmap's "proxy redesign" item; the DDoS history is the evidence base for where the line goes | M |
| F5 |
Guard rails + docs: CI check pinning the single-ldh:href()-chokepoint invariant (the location-transparency argument rests on it, Federation evaluation §4.5); when a proxied response carries no sd:endpoint Link, disable query-dependent blocks instead of silently querying the local endpoint about remote data (C3); multi-node deployment guidance |
S |
Now: P0.1–P0.5 (close open pen-test findings) → retest
This quarter: P1.1 / P1.2 / P1.5 / P1.6 + D3 demo (agentic portals — near-zero new code)
Next 2 qtrs: P2.2 + P2.3 ∥ I1 / I2 → D2 launch (MCP-native memory)
Background arc: P2.1 (v6 XHTML+RDFa, Phases A–E) — absorbs Bootstrap debt as generated CSS
Then: I3 / I4 / I5 → D1 as a nameable product (personal dataspace)
Federation arc: F2 done → F1 with I2 → F4 with the proxy redesign → F3 / F5 opportunistic
-
P0.1/P0.2/P0.3/P0.5: extend existing unit tests (
URLValidatorTest, new JWT tests);mvn test. Fullhttp-tests/via./run.sh ssl/owner/cert.pem [pw] ssl/secretary/cert.pem [pw]. Then request an ROS retest. - P1.4: index a body-only-text document, confirm the search modal finds it.
-
P1.5: confirm
/add,/transform,/generateremoval + client PATCH round-trips via http-tests; verify no server-side fetch remains. -
D3 demo: Web-Algebra
ldh-GeneratePortalagainst a VKG endpoint; confirm dashboard renders in LDH. - P2.1: http-tests asserting graph equivalence between stored XHTML+RDFa extraction and the current RDF write path (spec's round-trip fidelity check).
-
F2:
mvn clean package(recompiles the edited XSLT to SEF); then with the stack up: open a writable local document and a remote document (?uri=) in two tabs, switch back and forth — edit affordances (row-hover controls, drag-drop, Edit/Delete disabled state) must track the active pane, not the last-loaded document. Against a remote LDH node withacl:delegateswired, edit controls appear on the remote document and PATCH round-trips through the proxy.