This is an automated email from the ASF dual-hosted git repository.

luigidemasi pushed a commit to branch fix/security-model-verification-153-158
in repository https://gitbox.apache.org/repos/asf/camel-kit.git

commit 03cdfaeb358fbd6fc7a46125ff9fd5a2c3361df0
Author: Luigi De Masi <[email protected]>
AuthorDate: Mon Nov 9 19:15:20 2026 +0100

    Fix #154: confine and bound graph parser inputs and deadlines
    
    Fixes #154
    
    Co-authored-by: OpenAI Codex <[email protected]>
---
 CHANGELOG.md                                       |   2 +
 docs/modules/ROOT/pages/graph.adoc                 |  12 +
 docs/security-model.md                             |  25 +-
 docs/security-model.yaml                           |   9 +-
 docs/security-verification.md                      |  16 ++
 .../org/apache/camel/kit/graph/GraphBuilder.java   |  42 +++-
 .../camel/kit/graph/parser/BizTalkParser.java      |   4 +-
 .../camel/kit/graph/parser/ConfigParser.java       |   4 +-
 .../camel/kit/graph/parser/DataWeaveParser.java    |   4 +-
 .../apache/camel/kit/graph/parser/GraphParser.java | 106 ++++++++-
 .../camel/kit/graph/parser/GroovyGraphParser.java  |   5 +-
 .../camel/kit/graph/parser/JavaGraphParser.java    |   5 +-
 .../camel/kit/graph/parser/MuleXmlFlowParser.java  |   7 +-
 .../apache/camel/kit/graph/parser/PomParser.java   |   6 +-
 .../camel/kit/graph/parser/XmlRouteParser.java     |   7 +-
 .../camel/kit/graph/parser/YamlRouteParser.java    |   3 +-
 .../graph/parser/biztalk/BizTalkBindingParser.java |   4 +-
 .../kit/graph/parser/biztalk/BizTalkBtmParser.java |   4 +-
 .../kit/graph/parser/biztalk/BizTalkBtpParser.java |   4 +-
 .../kit/graph/parser/biztalk/BizTalkOdxParser.java |   3 +-
 .../camel/kit/graph/GraphInputBoundaryTest.java    | 252 +++++++++++++++++++++
 21 files changed, 484 insertions(+), 40 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d84d6f31..2862997a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,6 +22,8 @@ and this project adheres to [Semantic 
Versioning](https://semver.org/spec/v2.0.0
 
 ### Added
 
+- **Graph input boundaries (#154)** — reject outside-root and symlink inputs, 
bound source bytes and discovery, and apply the parser timeout to POM analysis.
+
 - **Parser source-reading regression coverage (#153)** — exercise all nine 
graph parsers with execution, class-loading, placeholder and external-entity 
canaries.
 
 - **Google Antigravity native Ship (#95)** — eligible sessions relay 
controller-issued stages through a dedicated
diff --git a/docs/modules/ROOT/pages/graph.adoc 
b/docs/modules/ROOT/pages/graph.adoc
index db7021d7..3f6d019f 100644
--- a/docs/modules/ROOT/pages/graph.adoc
+++ b/docs/modules/ROOT/pages/graph.adoc
@@ -143,3 +143,15 @@ architecture.
 * `camel-implement` loads `project-context` and `camel-test` loads 
`route-context` when a graph exists.
 * `/camel-validate` consumes `project-norms` and includes the `dead-code` 
report.
 * `/camel-start` and `/camel-knowledge` do not use it.
+
+== Input safety and limits
+
+Graph analysis reads sources without running the project. It skips 
symbolic-link files and
+directories below the selected root, rejects outside-root inputs, and accepts 
an explicitly
+selected root alias. Each source is limited to 16 MiB; recognized source files 
together are
+limited to 64 MiB, and discovery stops after 100,000 filesystem entries. Large 
unrelated
+binary artifacts do not count toward the source-byte limit.
+
+Each parser, including POM parsing, has a 60-second timeout. A timed-out 
parser contributes
+no partial result and produces a diagnostic. Cancellation is cooperative; 
these are input
+and waiting-time limits, not an operating-system memory or CPU sandbox.
diff --git a/docs/security-model.md b/docs/security-model.md
index 8427e7e5..28bfe1dc 100644
--- a/docs/security-model.md
+++ b/docs/security-model.md
@@ -349,7 +349,7 @@ a report against `0.0.2` is triaged on what that release 
documents.
 | Process (configuration) | Listening sockets | Child processes | Network | 
Filesystem writes | Environment read | Telemetry | Limits |
 | --- | --- | --- | --- | --- | --- | --- | --- |
 | **Kit CLI** `camel-kit` / `camel kit`: `init`, `doctor`, `doc`, `nextId`, 
`plan analyze`, `graph` | none (inspected) | version probes only: `java 
-version`, `jbang version`, `camel --version`, `mvn`/`mvnw`, `pi -a` 
*(documented — `PrerequisiteChecker`, `DoctorService`, 
`docs/agent-architectures.md`)* | `init` without `--no-fetch`: HTTPS to 
`repo1.maven.org`, `raw.githubusercontent.com/apache/camel`, `api.github.com` 
(Kamelet listing) and `search.maven.org`; redirects followed except HT [...]
-| **Graph analysis** (inside the CLI process) | none (inspected) | none 
*(documented — `ParserSourceReadingContractTest`; [V1 
evidence](security-verification.md#v1-source-reading))* | none *(documented — 
`ParserSourceReadingContractTest`; [V1 
evidence](security-verification.md#v1-source-reading))* | 
`.camel-kit/project-graph.json` or the `--output` path *(documented — 
`GraphGenerateCommand`)* | none (inspected) | none (inspected) | 60 s per 
parser except the POM parser; no file-size or m [...]
+| **Graph analysis** (inside the CLI process) | none (inspected) | none 
*(documented — `ParserSourceReadingContractTest`; [V1 
evidence](security-verification.md#v1-source-reading))* | none *(documented — 
`ParserSourceReadingContractTest`; [V1 
evidence](security-verification.md#v1-source-reading))* | 
`.camel-kit/project-graph.json` or the `--output` path *(documented — 
`GraphGenerateCommand`)* | none (inspected) | none (inspected) | 60 s per 
parser including POM; 16 MiB/source, 64 MiB agg [...]
 | **Ship controller** `camel-kit ship` | none (inspected) | `pi` and `node` 
(Pi backend), `/usr/bin/setsid`, `/bin/kill`, the controller's own `java` for 
evidence *(documented — `PiWorker`, `LocalCommandRunner`, `EvidenceRunner`)* | 
HTTPS to Maven Central through the resolver; no redirects; checksum `fail` 
*(documented — `ShipMavenResolver`)* | `.camel-kit/ship/state/` or 
`$CAMEL_KIT_SHIP_STATE_HOME`; the live project only through journaled 
publication *(documented — `docs/commands.md`,  [...]
 | **Ship Pi worker** (`--backend pi`) | Pi's own process | Pi's tools — 
`bash`, `edit`, `write` in EXECUTE — started in the staging cwd, which is a 
working directory, not a confinement *(documented — `PiWorker` Javadoc)* | 
whatever Pi and its provider need — external behaviour, outside this model 
(§4.3); launched with `--offline`, `--no-extensions`, `--no-skills`, 
`--no-context-files` *(documented — `PiWorker` arguments)* | *Intended:* the 
staging workspace and its session directory unde [...]
 | **Ship evidence JVMs** | none (inspected) | none (inspected) | none by 
design: every non-direct endpoint stubbed *(documented — `docs/commands.md`)* | 
private `HOME` and `TMPDIR` under the run; frozen read-only copy *(documented — 
`EvidenceRunner`)* | scrubbed to `LANG`, `LC_ALL`, `HOME`, `TMPDIR`, 
`GIT_CEILING_DIRECTORIES` and controller-provided variables *(documented — 
`EvidenceRunner`)* | none (inspected) | 64 MiB log cap; stage timeout 
*(documented — `EvidenceRunner`)* |
@@ -426,8 +426,7 @@ Per-entry-point trust table. "Caller" is the developer 
unless stated otherwise.
 | Knowledge configuration (`knowledge.index.*`, `KNOWLEDGE_INDEX_*`, 
`quarkus.http.*`) and a local `knowledge.index.path` | properties, environment, 
directory | no — operator | — |
 | Knowledge cache directory (`~/.camel-kit/knowledge-index`) | version 
directories, `current`, `etag`, staging | same user only | Server: version 
directories must be real directories; marker and validator rebound on 
activation; staging cleaned under the update lock (§4.8 P13) |
 
-**Size, shape and rate.** Ship states its limits (§4.8 P11). Graph analysis 
reads whole files; the per-parser timeout
-in P1 is the only bound in place today *(inferred — V2)*. `init` downloads are 
whole artifacts with a 30–60 s request timeout *(documented — downloader
+**Size, shape and rate.** Ship states its limits (§4.8 P11). Graph input 
bounds are 16 MiB per source, 64 MiB aggregate recognized source and 100,000 
filesystem entries; every parser has a 60-second timeout (P1). `init` downloads 
are whole artifacts with a 30–60 s request timeout *(documented — downloader
 constants)*.
 
 ---
@@ -496,11 +495,14 @@ violation warrants coordinated disclosure and a CVE; 
**hardening** means it is f
    `IS_SUPPORTING_EXTERNAL_ENTITIES=false` and `SUPPORT_DTD=false` 
*(documented — `BizTalk*Parser`)*; the Java parser
    is AST-only (JavaParser, no symbol solving) *(documented — 
`ParserSourceReadingContractTest`; [V1 
evidence](security-verification.md#v1-source-reading))*; the Groovy parser 
compiles to the `CONVERSION`
    phase only *(documented — `ParserSourceReadingContractTest`; [V1 
evidence](security-verification.md#v1-source-reading))*; YAML routes and 
configuration use Jackson's tree model, DataWeave and properties are
-   regular-expression scans *(documented — `ParserSourceReadingContractTest`; 
[V1 evidence](security-verification.md#v1-source-reading))*. Bound in place: 
`GraphBuilder` runs every parser other than the POM
-   parser as a task with a 60-second timeout (`DEFAULT_PARSER_TIMEOUT`); a 
parser that exceeds it is cancelled and
-   recorded as a timeout diagnostic *(documented — `GraphBuilder`)*. Not yet 
verified: a bound for the POM parser, a
-   memory or file-size bound, and confinement of file reads to the analysed 
root when the tree contains symbolic
-   links *(inferred — V2)*. *Violation symptom:* a child process, a class 
loaded or defined from the source, an
+   regular-expression scans *(documented — `ParserSourceReadingContractTest`; 
[V1 evidence](security-verification.md#v1-source-reading))*. `GraphBuilder` 
applies a 60-second timeout to every parser, including the POM parser.
+   A timeout discards that parser's partial result. `GraphParser` refuses file 
links and outside-root
+   inputs, bounds each source to 16 MiB, aggregate recognized source to 64 
MiB, and discovery to
+   100,000 filesystem entries. Reads enforce the per-file bound again. The 
selected root may itself
+   be an operator-chosen alias. See [V2 
evidence](security-verification.md#v2-graph-inputs).
+   Cancellation is cooperative inside a daemon thread; it bounds the caller's 
wait, not a hard JVM
+   heap or CPU quota. These controls do not claim protection against a 
same-account process
+   concurrently replacing directories. *Violation symptom:* a child process, a 
class loaded or defined from the source, an
    outbound connection, a read of a file outside the analysed root (including 
through a symbolic link), a
    placeholder resolved, non-termination, or unbounded memory during 
`camel-kit graph` or `init --here`.
    *Severity:* security-critical — the umbrella lists "reads a file" and "does 
not finish" among the breaches of this
@@ -634,7 +636,7 @@ violation warrants coordinated disclosure and a CVE; 
**hardening** means it is f
     ≤ 1 MiB, RPC log ≤ 16 MiB, session ≤ 64 MiB *(documented — `PiWorker`)*; 
evidence log ≤ 64 MiB and stage
     timeout 10 min default *(documented — `EvidenceRunner`, 
`docs/commands.md`)*; resolver bounds as in P4; tree
     quotas as in P6; `graph migration-context` "defaults to depth 3, and 
returns at most 50 expanded nodes"
-    *(documented — `docs/commands.md`)*; graph parsers other than the POM 
parser are bounded to 60 s each
+    *(documented — `docs/commands.md`)*; all graph parsers including the POM 
parser are bounded to 60 s each
     *(documented — `GraphBuilder`)*; Knowledge returns at most 25 results per 
call, reads release pages of at most
     2 MiB and at most 10 pages, and applies 3 s connect, 5 s manifest and 5 
min archive deadlines *(documented —
     `KnowledgeMcpServer`, `IndexDownloader`)*. *Violation:* a limit not 
enforced at its documented value. *Severity:*
@@ -1040,9 +1042,8 @@ own issue, linked below.
 
 - **V1 — Per-parser regression coverage (Q1; #153): implemented.** 
`ParserSourceReadingContractTest` exercises all nine parsers, with observable 
output and hostile source canaries. See [V1 
evidence](security-verification.md#v1-source-reading), including mutation 
controls. A demonstrated breach remains `VALID` under P1.
 
-- **V2 — Graph confinement and resource bounds (Q2; #154) → §4.6, §4.8 P1, 
P11.** Verify or add: confinement of file reads to
-  the analysed root when the tree contains symbolic links; the POM parser 
under the parser timeout; a memory or
-  file-size bound. *Interim triage:* a demonstrated outside-root read, 
non-termination or unbounded memory is `VALID`.
+- **V2 — Graph confinement and resource bounds (Q2; #154): implemented.** 
`GraphInputBoundaryTest` covers symlink/outside inputs, selected-root aliases, 
oversized files and aggregate input, large unrelated artifacts and the POM 
timeout. [V2 evidence](security-verification.md#v2-graph-inputs) states the 
bounds and cancellation limits. A demonstrated P1 breach remains `VALID`.
+
 - **V3 — Narrower Claude MCP grants (Q3; #155) → §4.5a, §4.8 P10, §4.11a.** 
Replace `mcp__camel__*`,
   `mcp__camel-knowledge__*` and `mcp__citrus__*` in the generated 
`settings.json` with explicit per-tool grants aligned
   with the workflow manifest's `allowed_tools`, and verify the generated 
behaviour in Claude Code. *Interim triage:* a
diff --git a/docs/security-model.yaml b/docs/security-model.yaml
index 4249a50b..622dc261 100644
--- a/docs/security-model.yaml
+++ b/docs/security-model.yaml
@@ -281,10 +281,10 @@ configuration_variants:
 
 claimed_properties:
   - id: P1
-    property: Source-reading contract for graph analysis, inherited in full 
from the umbrella model - no code execution, class loading, placeholder/secret 
resolution, network or child processes; always finishes in bounded time and 
memory; output is data. Bound in place - GraphBuilder 60-second timeout per 
parser other than the POM parser. Not yet verified - POM parser bound, memory 
or file-size bound, confinement of reads to the analysed root when the tree 
contains symbolic links (V2).
+    property: "Source-reading contract inherited from the umbrella model: no 
project code execution, project class loading, runtime placeholder/secret 
resolution, network or child processes; output is data. GraphParser confines 
inputs, refuses file links, limits source files to 16 MiB, recognized source 
total to 64 MiB and discovery to 100000 entries. GraphBuilder times out every 
parser including POM after 60 seconds, discards its partial result and cancels 
its daemon task. Cancellation  [...]
     violation_symptom: Child process, class loaded or defined from the source, 
outbound connection, read of a file outside the analysed root (including 
through a symbolic link), placeholder resolved, non-termination, or unbounded 
memory during graph analysis.
     severity: security-critical (VALID regardless of the verification status 
in V1/V2)
-    provenance: contract - documented (umbrella) and maintainer (2026-10-27, 
Q1 and Q2); XML/StAX hardening documented (SecureXml, BizTalk parsers); 
GraphBuilder timeout documented; all nine parsers covered by 
ParserSourceReadingContractTest (security-verification.md V1); confinement and 
resource enforcement inferred (V2)
+    provenance: "Contract: umbrella and maintainer (2026-10-27, Q1/Q2). 
Controls: GraphParser, GraphBuilder, SecureXml and BizTalk parsers. 
Regressions: ParserSourceReadingContractTest, GraphInputBoundaryTest; 
security-verification.md V1/V2."
   - id: P2
     property: Hardened XML parsing at every XML entry point, including Ship 
pom.xml validation.
     violation_symptom: XXE, SSRF or local file read through XML.
@@ -331,7 +331,7 @@ claimed_properties:
     severity: hardening; security-critical when the widened grant reaches a 
credential or destructive action without a prompt
     provenance: "documented (docs/agent-architectures.md, docs/ship-claude.md, 
docs/ship-native.md, templates/mcp-configs/*, templates/claude/settings.json, 
Claude Code permissions reference); current Claude grant - maintainer, 
2026-10-22 (review of #6); accepted posture - maintainer, 2026-10-27 (Q3)"
   - id: P11
-    property: Stated resource bounds - route/Citrus YAML 2 MiB, native result 
16 MiB and 1000 files, Pi prompt 1 MiB, Pi log 16 MiB, session 64 MiB, evidence 
log 64 MiB, stage timeout 10m, resolver 64 roots/512 artifacts/128 MiB JAR/4 
MiB POM, tree quotas, migration-context depth 3 and 50 nodes; graph parsers 
other than the POM parser 60 s each (serves P1); Knowledge tool results at most 
25 per call, release pages at most 2 MiB and 10 pages, manifest 5 s and archive 
5 min deadlines.
+    property: Stated resource bounds - route/Citrus YAML 2 MiB, native result 
16 MiB and 1000 files, Pi prompt 1 MiB, Pi log 16 MiB, session 64 MiB, evidence 
log 64 MiB, stage timeout 10m, resolver 64 roots/512 artifacts/128 MiB JAR/4 
MiB POM, tree quotas, migration-context depth 3 and 50 nodes; all graph parsers 
including POM 60 s each, source16 MiB/file and64 MiB total, discovery100000 
entries (serves P1); Knowledge tool results at most 25 per call, release pages 
at most 2 MiB and 10 p [...]
     violation_symptom: A limit not enforced at its documented value.
     severity: hardening
     provenance: documented (ShipArtifactLimits, ShipNativeWorker, PiWorker, 
EvidenceRunner, ShipMavenResolver, ShipTreePolicy, docs/commands.md, 
KnowledgeMcpServer, IndexDownloader)
@@ -589,7 +589,8 @@ verification_items:
     sections: ["4.6", "4.8 P1", "4.8 P11"]
     work: Verify or add - confinement of file reads to the analysed root when 
the tree contains symbolic links; the POM parser under the parser timeout; a 
memory or file-size bound.
     interim_triage: A demonstrated outside-root read, non-termination or 
unbounded memory is VALID under P1.
-    status: open
+    status: "implemented"
+    evidence: "GraphInputBoundaryTest and GraphBuilderTest; source/entry 
bounds and cooperative timeout limits documented in security-verification.md V2"
   - id: V3
     tracking: apache/camel-kit#155
     title: Narrower Claude MCP grants in the generated settings.json
diff --git a/docs/security-verification.md b/docs/security-verification.md
index 8af1e3fd..ad2f4bfc 100644
--- a/docs/security-verification.md
+++ b/docs/security-verification.md
@@ -19,3 +19,19 @@ process and enabling XML entity expansion all caused 
failures. The XML mutation
 text: an external entity in an attribute would be rejected even by an 
otherwise unsafe parser.
 These probes protect the covered execution paths; they do not prove safety of 
arbitrary future
 parser/dependency changes. Website impact: not required; this pins existing 
behavior.
+
+
+## V2: graph inputs
+
+`GraphInputBoundaryTest` exercises actual parsing and `GraphBuilder`: outside 
file links,
+directory links, an operator-selected root alias, direct supplied 
outside/oversized lists,
+16 MiB and 16 MiB+1 sources, aggregate overflow, large unrelated artifacts and 
a deliberately
+slow POM parser. Production bounds are 16 MiB per input, 64 MiB aggregate 
recognized source
+and 100,000 visited entries. Sources are checked before parser sniffing and 
bounded again
+when read. The POM pass uses the same timeout policy as the remaining parsers.
+
+The baseline reproduced file-link escape, missing POM timeout and 
oversized-input acceptance.
+A timed-out parser's fragment is discarded; daemon threads do not hold the CLI 
open.
+Cancellation remains cooperative, and parser/library expansion can exceed the 
input byte size:
+there is no hard heap quota. Filesystem checks cover a pre-existing hostile 
tree, not concurrent
+same-account directory substitution. Website impact: required; graph reference 
updated.
diff --git 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/GraphBuilder.java
 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/GraphBuilder.java
index 82804d9b..83ca27a6 100644
--- 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/GraphBuilder.java
+++ 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/GraphBuilder.java
@@ -78,15 +78,15 @@ public class GraphBuilder {
 
         // Run PomParser first so MAVEN_ARTIFACT nodes are available for all 
other parsers
         ParserRun pomRun
-                = runParser(pomParser, projectRoot, new ProjectGraph(),
-                        parserTask(pomParser, projectRoot, projectFiles));
+                = runFirstParser(projectRoot, projectFiles);
         diagnostics.add(pomRun.diagnostic());
         if (pomRun.diagnostic().successful()) {
             mergeDeterministically(graph, pomRun.graph());
         }
 
         ExecutorService executor = Executors.newFixedThreadPool(
-                Math.max(1, Math.min(remainingParsers.size(), 
Runtime.getRuntime().availableProcessors())));
+                Math.max(1, Math.min(remainingParsers.size(), 
Runtime.getRuntime().availableProcessors())),
+                GraphBuilder::parserThread);
         try {
             ProjectGraph baseGraph = new ProjectGraph();
             baseGraph.merge(graph);
@@ -109,7 +109,9 @@ public class GraphBuilder {
             for (int i = 0; i < futures.size(); i++) {
                 ParserFuture parserFuture = futures.get(i);
                 try {
-                    ParserRun parserRun = 
parserFuture.future().get(parserTimeout, parserTimeoutUnit);
+                    long remaining = parserTimeoutUnit.toNanos(parserTimeout)
+                                     - (System.nanoTime() - 
parserFuture.submittedAt());
+                    ParserRun parserRun = 
parserFuture.future().get(Math.max(0, remaining), TimeUnit.NANOSECONDS);
                     diagnostics.add(parserRun.diagnostic());
                     if (parserRun.diagnostic().successful()) {
                         mergeDeterministically(graph, parserRun.graph());
@@ -176,6 +178,9 @@ public class GraphBuilder {
         try {
             ProjectGraph fragment = parser.parseFragment(projectRoot, 
baseGraph, task.scannedFilePaths());
             delta = deltaFrom(fragment, baseGraph);
+            if (System.nanoTime() - startedAt > 
parserTimeoutUnit.toNanos(parserTimeout)) {
+                return new ParserRun(new ProjectGraph(), 
timeoutDiagnostic(parser, task.scannedFiles()));
+            }
             ParserDiagnostic diagnostic = diagnostic(
                     parser,
                     task.scannedFiles(),
@@ -193,6 +198,35 @@ public class GraphBuilder {
         }
     }
 
+    private ParserRun runFirstParser(Path projectRoot, List<Path> 
projectFiles) {
+        ParserTask task = parserTask(pomParser, projectRoot, projectFiles);
+        ExecutorService executor = 
Executors.newSingleThreadExecutor(GraphBuilder::parserThread);
+        Future<ParserRun> future = executor.submit(() -> runParser(pomParser, 
projectRoot, new ProjectGraph(), task));
+        long startedAt = System.nanoTime();
+        try {
+            return future.get(parserTimeout, parserTimeoutUnit);
+        } catch (TimeoutException e) {
+            return new ParserRun(new ProjectGraph(), 
timeoutDiagnostic(pomParser, task.scannedFiles()));
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            return new ParserRun(new ProjectGraph(), 
interruptedDiagnostic(pomParser, task.scannedFiles()));
+        } catch (ExecutionException e) {
+            return new ParserRun(
+                    new ProjectGraph(), failureDiagnostic(pomParser, 
task.scannedFiles(),
+                            e.getCause(), elapsedMillis(startedAt)));
+        } finally {
+            future.cancel(true);
+            executor.shutdownNow();
+        }
+    }
+
+    private static Thread parserThread(Runnable task) {
+        Thread thread = new Thread(task, "camel-kit-graph-parser");
+        // A timed-out library parser must not keep the CLI alive if it 
ignores interruption.
+        thread.setDaemon(true);
+        return thread;
+    }
+
     private ParserTask parserTask(GraphParser parser, Path projectRoot, 
List<Path> projectFiles) {
         try {
             List<Path> scannedFilePaths = parser.scannedFilePaths(projectRoot, 
projectFiles);
diff --git 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/BizTalkParser.java
 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/BizTalkParser.java
index 1283a00d..f5c4afb9 100644
--- 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/BizTalkParser.java
+++ 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/BizTalkParser.java
@@ -5,6 +5,7 @@ import java.io.InputStream;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.*;
+import java.nio.file.LinkOption;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
@@ -44,6 +45,7 @@ public class BizTalkParser implements GraphParser {
 
     @Override
     public void parseFiles(Path projectRoot, List<Path> files, ProjectGraph 
graph) {
+        GraphParser.requireSafeFiles(projectRoot, files);
         warnings.clear();
         files.forEach(file -> parseArtifact(projectRoot, file, graph));
     }
@@ -92,7 +94,7 @@ public class BizTalkParser implements GraphParser {
      * @return      true if the file contains BizTalk binding markers
      */
     private boolean isBizTalkBindingXml(Path file) {
-        try (InputStream is = Files.newInputStream(file)) {
+        try (InputStream is = Files.newInputStream(file, 
LinkOption.NOFOLLOW_LINKS)) {
             byte[] buf = new byte[SNIFF_BYTES];
             int read = is.read(buf);
             if (read <= 0) {
diff --git 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/ConfigParser.java
 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/ConfigParser.java
index ddcfa159..938d26f8 100644
--- 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/ConfigParser.java
+++ 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/ConfigParser.java
@@ -1,5 +1,6 @@
 package org.apache.camel.kit.graph.parser;
 
+import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.file.*;
@@ -27,6 +28,7 @@ public class ConfigParser implements GraphParser {
 
     @Override
     public void parseFiles(Path projectRoot, List<Path> files, ProjectGraph 
graph) {
+        GraphParser.requireSafeFiles(projectRoot, files);
         files.forEach(file -> parseProperties(file, projectRoot, graph));
     }
 
@@ -37,7 +39,7 @@ public class ConfigParser implements GraphParser {
 
     private void parseProperties(Path file, Path projectRoot, ProjectGraph 
graph) {
         Properties props = new Properties();
-        try (InputStream in = Files.newInputStream(file)) {
+        try (InputStream in = new 
ByteArrayInputStream(GraphParser.readSource(file))) {
             props.load(in);
         } catch (IOException e) {
             throw new RuntimeException("Failed to parse properties: " + file, 
e);
diff --git 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/DataWeaveParser.java
 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/DataWeaveParser.java
index 9a16e801..cd18d4f0 100644
--- 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/DataWeaveParser.java
+++ 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/DataWeaveParser.java
@@ -1,6 +1,7 @@
 package org.apache.camel.kit.graph.parser;
 
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.*;
 import java.util.*;
 import java.util.regex.Matcher;
@@ -43,13 +44,14 @@ public class DataWeaveParser implements GraphParser {
 
     @Override
     public void parseFiles(Path projectRoot, List<Path> files, ProjectGraph 
graph) {
+        GraphParser.requireSafeFiles(projectRoot, files);
         files.forEach(file -> parseDwlFile(file, projectRoot, graph));
     }
 
     private void parseDwlFile(Path file, Path projectRoot, ProjectGraph graph) 
{
         String content;
         try {
-            content = Files.readString(file);
+            content = new String(GraphParser.readSource(file), 
StandardCharsets.UTF_8);
         } catch (IOException e) {
             throw new RuntimeException("Failed to read DataWeave file: " + 
file, e);
         }
diff --git 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/GraphParser.java
 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/GraphParser.java
index 0f3cf1f0..5a75df81 100644
--- 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/GraphParser.java
+++ 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/GraphParser.java
@@ -1,15 +1,23 @@
 package org.apache.camel.kit.graph.parser;
 
 import java.io.IOException;
+import java.io.InputStream;
 import java.nio.file.Files;
+import java.nio.file.LinkOption;
 import java.nio.file.Path;
+import java.util.ArrayList;
 import java.util.List;
+import java.util.Locale;
 import java.util.function.Predicate;
 import java.util.stream.Stream;
 
 import org.apache.camel.kit.graph.ProjectGraph;
 
 public interface GraphParser {
+    int MAX_SOURCE_BYTES = 16 * 1024 * 1024;
+    long MAX_TOTAL_SOURCE_BYTES = 64L * 1024 * 1024;
+    int MAX_PROJECT_ENTRIES = 100_000;
+
     void parse(Path projectRoot, ProjectGraph graph);
 
     default ProjectGraph parseFragment(Path projectRoot, ProjectGraph 
baseGraph) {
@@ -49,11 +57,35 @@ public interface GraphParser {
             return List.of();
         }
 
-        try (Stream<Path> paths = Files.walk(projectRoot)) {
-            return paths
-                    .filter(Files::isRegularFile)
-                    .sorted()
-                    .toList();
+        Path realRoot;
+        try {
+            realRoot = projectRoot.toRealPath();
+        } catch (IOException e) {
+            throw new IllegalArgumentException("Cannot resolve graph project 
root: " + projectRoot, e);
+        }
+        try (Stream<Path> paths = Files.walk(realRoot)) {
+            List<Path> files = new ArrayList<>();
+            var iterator = paths.iterator();
+            int entries = 0;
+            long sourceBytes = 0;
+            while (iterator.hasNext()) {
+                Path path = 
projectRoot.resolve(realRoot.relativize(iterator.next()));
+                if (++entries > MAX_PROJECT_ENTRIES) {
+                    throw new IllegalArgumentException(
+                            "Project exceeds " + MAX_PROJECT_ENTRIES + " 
filesystem entries");
+                }
+                if (Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) {
+                    if (isSourceFile(path)) {
+                        sourceBytes += requireSafeFile(projectRoot, path);
+                        if (sourceBytes > MAX_TOTAL_SOURCE_BYTES) {
+                            throw new IllegalArgumentException(
+                                    "Project source exceeds " + 
MAX_TOTAL_SOURCE_BYTES + " bytes");
+                        }
+                    }
+                    files.add(path);
+                }
+            }
+            return files.stream().sorted().toList();
         } catch (IOException e) {
             throw new RuntimeException("Failed to scan files under " + 
projectRoot, e);
         }
@@ -65,11 +97,75 @@ public interface GraphParser {
 
     static List<Path> findFilePaths(Path projectRoot, List<Path> projectFiles, 
Predicate<Path> matcher) {
         return projectFiles.stream()
+                .peek(file -> {
+                    if (isSourceFile(file)) {
+                        requireSafeFile(projectRoot, file);
+                    }
+                })
                 .filter(matcher)
+                .peek(file -> requireSafeFile(projectRoot, file))
                 .sorted()
                 .toList();
     }
 
+    static void requireSafeFiles(Path projectRoot, List<Path> files) {
+        long bytes = 0;
+        if (files.size() > MAX_PROJECT_ENTRIES) {
+            throw new IllegalArgumentException("Too many graph input files");
+        }
+        for (Path file : files) {
+            bytes += requireSafeFile(projectRoot, file);
+            if (bytes > MAX_TOTAL_SOURCE_BYTES) {
+                throw new IllegalArgumentException("Project source exceeds " + 
MAX_TOTAL_SOURCE_BYTES + " bytes");
+            }
+        }
+    }
+
+    private static long requireSafeFile(Path projectRoot, Path file) {
+        Path root = projectRoot.toAbsolutePath().normalize();
+        Path input = file.toAbsolutePath().normalize();
+        if (!input.startsWith(root) || input.equals(root)) {
+            throw new IllegalArgumentException("Graph input is outside the 
project: " + file);
+        }
+        Path current = root;
+        for (Path component : root.relativize(input)) {
+            current = current.resolve(component);
+            if (Files.isSymbolicLink(current)) {
+                throw new IllegalArgumentException("Graph input is a symbolic 
link: " + current);
+            }
+        }
+        try {
+            if (!Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)
+                    || 
!file.toRealPath().startsWith(projectRoot.toRealPath())) {
+                throw new IllegalArgumentException("Graph input is not a 
confined regular file: " + file);
+            }
+            long bytes = Files.size(file);
+            if (bytes > MAX_SOURCE_BYTES) {
+                throw new IllegalArgumentException("Graph input exceeds " + 
MAX_SOURCE_BYTES + " bytes: " + file);
+            }
+            return bytes;
+        } catch (IOException e) {
+            throw new IllegalArgumentException("Cannot validate graph input: " 
+ file, e);
+        }
+    }
+
+    static byte[] readSource(Path file) throws IOException {
+        try (InputStream input = Files.newInputStream(file, 
LinkOption.NOFOLLOW_LINKS)) {
+            byte[] bytes = input.readNBytes(MAX_SOURCE_BYTES + 1);
+            if (bytes.length > MAX_SOURCE_BYTES) {
+                throw new IOException("Graph input exceeds " + 
MAX_SOURCE_BYTES + " bytes: " + file);
+            }
+            return bytes;
+        }
+    }
+
+    private static boolean isSourceFile(Path file) {
+        String name = file.getFileName().toString().toLowerCase(Locale.ROOT);
+        return name.endsWith(".java") || name.endsWith(".groovy") || 
name.endsWith(".xml")
+                || name.endsWith(".yaml") || name.endsWith(".yml") || 
name.endsWith(".properties")
+                || name.endsWith(".dwl") || name.endsWith(".odx") || 
name.endsWith(".btm") || name.endsWith(".btp");
+    }
+
     static List<String> relativeFileNames(Path projectRoot, List<Path> files) {
         return files.stream()
                 .map(file -> relativeFileName(projectRoot, file))
diff --git 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/GroovyGraphParser.java
 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/GroovyGraphParser.java
index 2d5250d3..7f87f3cb 100644
--- 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/GroovyGraphParser.java
+++ 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/GroovyGraphParser.java
@@ -1,6 +1,7 @@
 package org.apache.camel.kit.graph.parser;
 
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.*;
 import java.util.*;
 
@@ -40,6 +41,7 @@ public class GroovyGraphParser implements GraphParser {
 
     @Override
     public void parseFiles(Path projectRoot, List<Path> files, ProjectGraph 
graph) {
+        GraphParser.requireSafeFiles(projectRoot, files);
         files.forEach(file -> parseGroovyFile(file, projectRoot, graph));
     }
 
@@ -51,7 +53,8 @@ public class GroovyGraphParser implements GraphParser {
                 Thread.currentThread().getContextClassLoader(), config)) {
 
             CompilationUnit compilationUnit = new CompilationUnit(config, 
null, classLoader);
-            compilationUnit.addSource(file.toFile());
+            compilationUnit.addSource(file.toString(),
+                    new String(GraphParser.readSource(file), 
StandardCharsets.UTF_8));
             compilationUnit.compile(Phases.CONVERSION);
 
             // Get the modules from the compilation unit
diff --git 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/JavaGraphParser.java
 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/JavaGraphParser.java
index 8f41c7f8..2e1a70a9 100644
--- 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/JavaGraphParser.java
+++ 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/JavaGraphParser.java
@@ -1,6 +1,7 @@
 package org.apache.camel.kit.graph.parser;
 
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.*;
 import java.util.*;
 import java.util.regex.Matcher;
@@ -49,6 +50,7 @@ public class JavaGraphParser implements GraphParser {
 
     @Override
     public void parseFiles(Path projectRoot, List<Path> files, ProjectGraph 
graph) {
+        GraphParser.requireSafeFiles(projectRoot, files);
         JavaParser parser = new JavaParser();
         files.forEach(file -> parseJavaFile(parser, file, projectRoot, graph));
     }
@@ -75,7 +77,8 @@ public class JavaGraphParser implements GraphParser {
 
     private void parseJavaFile(JavaParser parser, Path file, Path projectRoot, 
ProjectGraph graph) {
         try {
-            var parseResult = parser.parse(file);
+            var parseResult
+                    = parser.parse(new String(GraphParser.readSource(file), 
StandardCharsets.UTF_8));
             if (parseResult.getResult().isEmpty()) {
                 String problems = parseResult.getProblems().stream()
                         .map(Object::toString)
diff --git 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/MuleXmlFlowParser.java
 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/MuleXmlFlowParser.java
index c9e251ae..e40881c4 100644
--- 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/MuleXmlFlowParser.java
+++ 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/MuleXmlFlowParser.java
@@ -1,9 +1,11 @@
 package org.apache.camel.kit.graph.parser;
 
+import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.*;
+import java.nio.file.LinkOption;
 import java.util.*;
 
 import javax.xml.parsers.DocumentBuilder;
@@ -57,6 +59,7 @@ public class MuleXmlFlowParser implements GraphParser {
 
     @Override
     public void parseFiles(Path projectRoot, List<Path> files, ProjectGraph 
graph) {
+        GraphParser.requireSafeFiles(projectRoot, files);
         // Two passes across ALL files: first declare every flow/sub-flow 
node, then parse flow bodies, so
         // flow-ref targets resolve regardless of file processing order.
         List<ParsedMuleDocument> documents = new ArrayList<>();
@@ -90,7 +93,7 @@ public class MuleXmlFlowParser implements GraphParser {
      * Namespace sniff: read first 1KB and check for the MuleSoft namespace 
marker.
      */
     private boolean isMuleXml(Path file) {
-        try (InputStream is = Files.newInputStream(file)) {
+        try (InputStream is = Files.newInputStream(file, 
LinkOption.NOFOLLOW_LINKS)) {
             byte[] buf = new byte[SNIFF_BYTES];
             int read = is.read(buf);
             if (read <= 0)
@@ -107,7 +110,7 @@ public class MuleXmlFlowParser implements GraphParser {
         try {
             var factory = SecureXml.documentBuilderFactory();
             DocumentBuilder builder = factory.newDocumentBuilder();
-            Document doc = builder.parse(xmlFile.toFile());
+            Document doc = builder.parse(new 
ByteArrayInputStream(GraphParser.readSource(xmlFile)));
             String relPath = projectRoot.relativize(xmlFile).toString();
             return Optional.of(new ParsedMuleDocument(relPath, 
elementChildren(doc.getDocumentElement())));
         } catch (Exception e) {
diff --git 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/PomParser.java
 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/PomParser.java
index 7f942b20..63ac46dd 100644
--- 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/PomParser.java
+++ 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/PomParser.java
@@ -1,6 +1,8 @@
 package org.apache.camel.kit.graph.parser;
 
 import java.io.Reader;
+import java.io.StringReader;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.*;
 import java.util.List;
 import java.util.Map;
@@ -32,11 +34,13 @@ public class PomParser implements GraphParser {
 
     @Override
     public void parseFiles(Path projectRoot, List<Path> files, ProjectGraph 
graph) {
+        GraphParser.requireSafeFiles(projectRoot, files);
         files.forEach(file -> parsePom(file, projectRoot, graph));
     }
 
     private void parsePom(Path pomFile, Path projectRoot, ProjectGraph graph) {
-        try (Reader reader = Files.newBufferedReader(pomFile)) {
+        try (Reader reader = new StringReader(
+                new String(GraphParser.readSource(pomFile), 
StandardCharsets.UTF_8))) {
             Model model = new MavenXpp3Reader().read(reader);
 
             String groupId = model.getGroupId() != null
diff --git 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/XmlRouteParser.java
 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/XmlRouteParser.java
index 510a12f2..f338ea2b 100644
--- 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/XmlRouteParser.java
+++ 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/XmlRouteParser.java
@@ -1,9 +1,11 @@
 package org.apache.camel.kit.graph.parser;
 
+import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.*;
+import java.nio.file.LinkOption;
 import java.util.*;
 
 import javax.xml.parsers.DocumentBuilder;
@@ -40,6 +42,7 @@ public class XmlRouteParser implements GraphParser {
 
     @Override
     public void parseFiles(Path projectRoot, List<Path> files, ProjectGraph 
graph) {
+        GraphParser.requireSafeFiles(projectRoot, files);
         files.forEach(file -> parseXmlFile(file, projectRoot, graph));
     }
 
@@ -71,7 +74,7 @@ public class XmlRouteParser implements GraphParser {
         try {
             var factory = SecureXml.documentBuilderFactory();
             DocumentBuilder builder = factory.newDocumentBuilder();
-            Document doc = builder.parse(xmlFile.toFile());
+            Document doc = builder.parse(new 
ByteArrayInputStream(GraphParser.readSource(xmlFile)));
 
             // Find all route elements
             NodeList routes = doc.getElementsByTagNameNS("*", "route");
@@ -86,7 +89,7 @@ public class XmlRouteParser implements GraphParser {
     }
 
     private String readHead(Path file) throws IOException {
-        try (InputStream input = Files.newInputStream(file)) {
+        try (InputStream input = Files.newInputStream(file, 
LinkOption.NOFOLLOW_LINKS)) {
             byte[] buffer = new byte[SNIFF_BYTES];
             int read = input.read(buffer);
             if (read <= 0) {
diff --git 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/YamlRouteParser.java
 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/YamlRouteParser.java
index b7c54729..feca4475 100644
--- 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/YamlRouteParser.java
+++ 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/YamlRouteParser.java
@@ -36,6 +36,7 @@ public class YamlRouteParser implements GraphParser {
 
     @Override
     public void parseFiles(Path projectRoot, List<Path> files, ProjectGraph 
graph) {
+        GraphParser.requireSafeFiles(projectRoot, files);
         files.forEach(file -> parseYamlFile(file, projectRoot, graph));
     }
 
@@ -57,7 +58,7 @@ public class YamlRouteParser implements GraphParser {
 
     private void parseYamlFile(Path yamlFile, Path projectRoot, ProjectGraph 
graph) {
         try {
-            JsonNode root = yamlMapper.readTree(yamlFile.toFile());
+            JsonNode root = 
yamlMapper.readTree(GraphParser.readSource(yamlFile));
 
             // Handle array of routes or single route
             if (root.isArray()) {
diff --git 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/biztalk/BizTalkBindingParser.java
 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/biztalk/BizTalkBindingParser.java
index 82bc94c2..7ca45833 100644
--- 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/biztalk/BizTalkBindingParser.java
+++ 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/biztalk/BizTalkBindingParser.java
@@ -1,5 +1,6 @@
 package org.apache.camel.kit.graph.parser.biztalk;
 
+import java.io.ByteArrayInputStream;
 import java.io.InputStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -66,7 +67,8 @@ public class BizTalkBindingParser {
         factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, 
false);
         factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
 
-        try (InputStream input = Files.newInputStream(bindingFile)) {
+        try (InputStream input = new ByteArrayInputStream(
+                
org.apache.camel.kit.graph.parser.GraphParser.readSource(bindingFile))) {
             XMLStreamReader reader = factory.createXMLStreamReader(input);
 
             try {
diff --git 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/biztalk/BizTalkBtmParser.java
 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/biztalk/BizTalkBtmParser.java
index bec8fd6d..e00c07e4 100644
--- 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/biztalk/BizTalkBtmParser.java
+++ 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/biztalk/BizTalkBtmParser.java
@@ -1,5 +1,6 @@
 package org.apache.camel.kit.graph.parser.biztalk;
 
+import java.io.ByteArrayInputStream;
 import java.io.InputStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -130,7 +131,8 @@ public class BizTalkBtmParser {
         factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, 
false);
         factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
 
-        try (InputStream input = Files.newInputStream(btmFile)) {
+        try (InputStream input
+                = new 
ByteArrayInputStream(org.apache.camel.kit.graph.parser.GraphParser.readSource(btmFile)))
 {
             XMLStreamReader reader = factory.createXMLStreamReader(input);
 
             try {
diff --git 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/biztalk/BizTalkBtpParser.java
 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/biztalk/BizTalkBtpParser.java
index cd3a333f..34cb9b8f 100644
--- 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/biztalk/BizTalkBtpParser.java
+++ 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/biztalk/BizTalkBtpParser.java
@@ -1,5 +1,6 @@
 package org.apache.camel.kit.graph.parser.biztalk;
 
+import java.io.ByteArrayInputStream;
 import java.io.InputStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -89,7 +90,8 @@ public class BizTalkBtpParser {
         factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, 
false);
         factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
 
-        try (InputStream input = Files.newInputStream(btpFile)) {
+        try (InputStream input
+                = new 
ByteArrayInputStream(org.apache.camel.kit.graph.parser.GraphParser.readSource(btpFile)))
 {
             XMLStreamReader reader = factory.createXMLStreamReader(input);
 
             try {
diff --git 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/biztalk/BizTalkOdxParser.java
 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/biztalk/BizTalkOdxParser.java
index 46021a66..723ab8ca 100644
--- 
a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/biztalk/BizTalkOdxParser.java
+++ 
b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/biztalk/BizTalkOdxParser.java
@@ -53,7 +53,8 @@ public class BizTalkOdxParser {
                 return;
             }
 
-            String content = Files.readString(odxFile, StandardCharsets.UTF_8);
+            String content = new String(
+                    
org.apache.camel.kit.graph.parser.GraphParser.readSource(odxFile), 
StandardCharsets.UTF_8);
             String xmlContent = extractXmlContent(content);
             if (xmlContent == null || xmlContent.isEmpty()) {
                 return;
diff --git 
a/kit/camel-kit-graph/src/test/java/org/apache/camel/kit/graph/GraphInputBoundaryTest.java
 
b/kit/camel-kit-graph/src/test/java/org/apache/camel/kit/graph/GraphInputBoundaryTest.java
new file mode 100644
index 00000000..9fcc32e3
--- /dev/null
+++ 
b/kit/camel-kit-graph/src/test/java/org/apache/camel/kit/graph/GraphInputBoundaryTest.java
@@ -0,0 +1,252 @@
+package org.apache.camel.kit.graph;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.kit.graph.model.GraphNode;
+import org.apache.camel.kit.graph.model.NodeType;
+import org.apache.camel.kit.graph.parser.GraphParser;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class GraphInputBoundaryTest {
+
+    private static final int MAX_SOURCE_BYTES = 16 * 1024 * 1024;
+
+    @Test
+    void selectedRootMayBeASymbolicLink(@TempDir Path parent) throws Exception 
{
+        Path root = Files.createDirectory(parent.resolve("real"));
+        Files.writeString(root.resolve("application.properties"), 
"inside.canary=ordinary-value\n");
+        Path alias = Files.createSymbolicLink(parent.resolve("selected"), 
root);
+        assertTrue(new 
GraphBuilder().build(alias).hasNode("config:inside.canary"));
+    }
+
+    @Test
+    void aggregateSourceLimitIsCheckedBeforeParsing(@TempDir Path root) throws 
Exception {
+        for (int i = 0; i < 5; i++) {
+            try (var file
+                    = new java.io.RandomAccessFile(root.resolve("application-" 
+ i + ".properties").toFile(), "rw")) {
+                file.setLength(MAX_SOURCE_BYTES);
+            }
+        }
+        GraphBuildResult result = new 
GraphBuilder().buildWithDiagnostics(root);
+        assertFalse(result.successful());
+        assertTrue(result.failureSummary().contains("Project source exceeds"));
+        assertEquals(0, result.graph().nodeCount());
+    }
+
+    @Test
+    void suppliedFileListsCannotBypassConfinementOrSizeBounds(@TempDir Path 
parent) throws Exception {
+        Path root = Files.createDirectory(parent.resolve("project"));
+        Path outside = Files.writeString(parent.resolve("outside.properties"), 
"secret=value");
+        Path oversized = root.resolve("oversized.properties");
+        try (var file = new java.io.RandomAccessFile(oversized.toFile(), 
"rw")) {
+            file.setLength(MAX_SOURCE_BYTES + 1L);
+        }
+        List<GraphParser> parsers = List.of(
+                new org.apache.camel.kit.graph.parser.PomParser(),
+                new org.apache.camel.kit.graph.parser.JavaGraphParser(),
+                new org.apache.camel.kit.graph.parser.GroovyGraphParser(),
+                new org.apache.camel.kit.graph.parser.XmlRouteParser(),
+                new org.apache.camel.kit.graph.parser.MuleXmlFlowParser(),
+                new org.apache.camel.kit.graph.parser.DataWeaveParser(),
+                new org.apache.camel.kit.graph.parser.BizTalkParser(),
+                new org.apache.camel.kit.graph.parser.YamlRouteParser(),
+                new org.apache.camel.kit.graph.parser.ConfigParser());
+        for (GraphParser parser : parsers) {
+            assertThrows(IllegalArgumentException.class,
+                    () -> parser.parseFiles(root, List.of(outside), new 
ProjectGraph()));
+            assertThrows(IllegalArgumentException.class,
+                    () -> parser.parseFiles(root, List.of(oversized), new 
ProjectGraph()));
+        }
+        assertThrows(IllegalArgumentException.class,
+                () -> GraphParser.requireSafeFiles(root, 
java.util.Collections.nCopies(100_001, oversized)));
+    }
+
+    @Test
+    void suppliedPathsCannotHideAnOutsideSymlinkBehindAParentSegment(@TempDir 
Path parent) throws Exception {
+        Path root = Files.createDirectory(parent.resolve("project"));
+        Path outside = Files.createDirectory(parent.resolve("outside"));
+        Path outsideChild = Files.createDirectory(outside.resolve("child"));
+        Files.writeString(root.resolve("application.properties"), 
"inside.canary=ordinary-value\n");
+        Files.writeString(outside.resolve("application.properties"), 
"outside.canary=private-value\n");
+        Path link = Files.createSymbolicLink(root.resolve("link"), 
outsideChild);
+        Path input = link.resolve("../application.properties");
+        assertEquals(root.resolve("application.properties"), 
input.normalize());
+        assertEquals(outside.resolve("application.properties"), 
input.toRealPath());
+        ProjectGraph graph = new ProjectGraph();
+
+        assertThrows(IllegalArgumentException.class,
+                () -> new 
org.apache.camel.kit.graph.parser.ConfigParser().parseFiles(root, 
List.of(input), graph));
+        assertFalse(graph.hasNode("config:outside.canary"));
+    }
+
+    @Test
+    void outsideRootFileSymlinkCannotIntroducePrivateProperties(@TempDir Path 
tempDir) throws Exception {
+        Path root = Files.createDirectory(tempDir.resolve("project"));
+        Path sibling = Files.createDirectory(tempDir.resolve("project-other"));
+        Path outside = 
Files.writeString(sibling.resolve("application.properties"), 
"outside.canary=private-value\n");
+        Files.writeString(root.resolve("application-local.properties"), 
"inside.canary=ordinary-value\n");
+        Files.createSymbolicLink(root.resolve("application.properties"), 
root.relativize(outside));
+
+        GraphBuildResult result = new 
GraphBuilder().buildWithDiagnostics(root);
+
+        assertFalse(result.graph().hasNode("config:outside.canary"),
+                "An input filename inside the project must not authorize 
reading its outside symlink target");
+        assertTrue(result.graph().getNodes().values().stream()
+                .noneMatch(node -> 
node.properties().containsValue("private-value")));
+    }
+
+    @Test
+    void directorySymlinkCannotIntroduceOutsideProperties(@TempDir Path 
tempDir) throws Exception {
+        Path root = Files.createDirectory(tempDir.resolve("project"));
+        Path outside = Files.createDirectory(tempDir.resolve("outside"));
+        Files.writeString(outside.resolve("application.properties"), 
"outside.canary=private-value\n");
+        Files.createSymbolicLink(root.resolve("linked-directory"), outside);
+
+        GraphBuildResult result = new 
GraphBuilder().buildWithDiagnostics(root);
+
+        assertFalse(result.graph().hasNode("config:outside.canary"));
+    }
+
+    @Test
+    void ordinaryInputStillProducesItsGraph(@TempDir Path root) throws 
Exception {
+        Files.writeString(root.resolve("application.properties"), 
"inside.canary=ordinary-value\n");
+
+        GraphBuildResult result = new 
GraphBuilder().buildWithDiagnostics(root);
+
+        assertTrue(result.successful(), result::failureSummary);
+        assertEquals("ordinary-value", 
result.graph().getNode("config:inside.canary").properties().get("value"));
+    }
+
+    @Test
+    void pomParserUsesTheSameTimeoutAndCannotPublishItsPartialGraph(@TempDir 
Path root) {
+        GraphBuilder builder = new GraphBuilder(new SlowPomParser(), 
List.of(), 20, TimeUnit.MILLISECONDS);
+
+        GraphBuildResult result = builder.buildWithDiagnostics(root);
+        ParserDiagnostic pom = result.diagnostics().stream()
+                .filter(diagnostic -> 
diagnostic.parserName().equals("SlowPomParser"))
+                .findFirst().orElseThrow();
+
+        assertTrue(pom.timedOut(), "The first parser must obey the configured 
parser budget too");
+        assertFalse(result.successful());
+        assertFalse(result.graph().hasNode("maven:partial:timeout"),
+                "A cancelled parser cannot publish partial results even if it 
returns after interruption");
+    }
+
+    @Test
+    void laterParserCannotReceiveAFreshBudgetAfterTheFirstTimesOut(@TempDir 
Path root) {
+        CountDownLatch firstCancelled = new CountDownLatch(1);
+        GraphParser first = (project, graph) -> {
+            try {
+                firstCancelled.await(5, TimeUnit.SECONDS);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            } finally {
+                firstCancelled.countDown();
+            }
+        };
+        GraphParser second = (project, graph) -> {
+            try {
+                firstCancelled.await(5, TimeUnit.SECONDS);
+                // Complete after the first timeout, still inside the old, 
incorrectly renewed budget.
+                Thread.sleep(50);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            }
+            graph.addNode(new GraphNode("class:over-budget", NodeType.CLASS, 
Map.of()));
+        };
+        GraphBuilder builder = new GraphBuilder((project, graph) -> {
+        }, List.of(first, second),
+                200, TimeUnit.MILLISECONDS);
+
+        GraphBuildResult result = builder.buildWithDiagnostics(root);
+
+        assertTrue(result.diagnostics().get(1).timedOut());
+        assertTrue(result.diagnostics().get(2).timedOut(), "Waiting for 
another parser must not renew this budget");
+        assertFalse(result.graph().hasNode("class:over-budget"));
+    }
+
+    @Test
+    void sourceAtTheDocumentedLimitIsAccepted(@TempDir Path root) throws 
Exception {
+        Path source = root.resolve("application.properties");
+        writeProperties(source, MAX_SOURCE_BYTES);
+
+        GraphBuildResult result = new 
GraphBuilder().buildWithDiagnostics(root);
+
+        assertEquals(MAX_SOURCE_BYTES, Files.size(source));
+        assertTrue(result.successful(), result::failureSummary);
+        assertEquals("ordinary-value", 
result.graph().getNode("config:inside.canary").properties().get("value"));
+    }
+
+    @Test
+    void sourceOneByteOverTheLimitFailsWithoutPublishingItsContents(@TempDir 
Path root) throws Exception {
+        Path source = root.resolve("application.properties");
+        writeProperties(source, MAX_SOURCE_BYTES + 1);
+
+        GraphBuildResult result = new 
GraphBuilder().buildWithDiagnostics(root);
+
+        assertEquals(MAX_SOURCE_BYTES + 1L, Files.size(source));
+        assertFalse(result.successful(), "Oversized input must produce a 
diagnostic instead of being parsed");
+        assertFalse(result.graph().hasNode("config:inside.canary"));
+        assertFalse(result.failedDiagnostics().isEmpty());
+    }
+
+    @Test
+    void unrelatedLargeArtifactDoesNotPreventParsingSources(@TempDir Path 
root) throws Exception {
+        Files.writeString(root.resolve("application.properties"), 
"inside.canary=ordinary-value\n");
+        try (FileChannel artifact = 
FileChannel.open(root.resolve("application.jar"),
+                StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
+            artifact.position(MAX_SOURCE_BYTES);
+            artifact.write(ByteBuffer.wrap(new byte[]{0}));
+        }
+
+        GraphBuildResult result = new 
GraphBuilder().buildWithDiagnostics(root);
+
+        assertTrue(result.successful(), result::failureSummary);
+        assertEquals("ordinary-value", 
result.graph().getNode("config:inside.canary").properties().get("value"));
+    }
+
+    private static void writeProperties(Path file, int bytes) throws 
IOException {
+        byte[] property = 
"inside.canary=ordinary-value\n".getBytes(StandardCharsets.UTF_8);
+        byte[] comment = new byte[8192];
+        Arrays.fill(comment, (byte) ' ');
+        comment[0] = '#';
+        comment[comment.length - 1] = '\n';
+        try (OutputStream output = Files.newOutputStream(file)) {
+            output.write(property);
+            int remaining = bytes - property.length;
+            while (remaining > 0) {
+                int length = Math.min(remaining, comment.length);
+                output.write(comment, 0, length);
+                remaining -= length;
+            }
+        }
+    }
+
+    private static final class SlowPomParser implements GraphParser {
+        @Override
+        public void parse(Path root, ProjectGraph graph) {
+            try {
+                Thread.sleep(200);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            }
+            graph.addNode(new GraphNode("maven:partial:timeout", 
NodeType.MAVEN_ARTIFACT, Map.of()));
+        }
+    }
+}

Reply via email to