Git file transport must be constrained during submodule recursion
A Git repository can contain another repository as a submodule. The superproject's .gitmodules file describes the submodule name, path, and URL. During recursive clone or update, Git may follow that URL without a person typing it into a shell. That makes repository transport policy part of the build and CI trust boundary.
Git supports several transports. file can refer to a local path or a file:// URL, while http, https, ssh, and git cover networked repositories. The configuration documentation describes protocol policies that distinguish direct user actions from commands that clone or fetch on the user's behalf. A transport that is reasonable for a developer can be inappropriate during automatic recursive submodule initialization.
The trust question
The risk is not that every submodule is malicious. The risk is that a superproject controls a URL that Git will use before the dependent repository has been reviewed. A file: URL can point into the runner's filesystem, a mounted cache, or another repository that happens to be present. Even if the fetched content is not executed immediately, it can enter the build context or expose repository metadata and paths.
CI configurations often separate the checkout of the main project from the fetching of dependencies. A recursive clone may collapse that separation. The superproject's URL is remote input, but the resulting file access happens with the runner's local privileges.
Git's configuration documentation explains that protocol policies can be set globally, per command, or per protocol. It recommends a restrictive policy for commands that execute clone and fetch without user input, such as recursive submodule initialization. The exact setting should be imposed by the execution environment rather than left to a repository's local configuration.
Review the pipeline
Inspect every CI job that uses --recurse-submodules, git submodule update --init, or an equivalent library call. Determine the effective protocol.allow and protocol.file.allow values, including configuration inherited from the runner and any repository-local overrides. Record whether the job consumes a pull request from an untrusted fork and whether it has access to secrets or production mounts.
Check how the checkout action handles submodules. Some actions use a separate token, a shallow fetch, or a filtered archive. Those choices change which URLs and credentials are available. Confirm that a nested submodule cannot point at a local credential store or an unrelated checkout.
Some projects need a private local mirror for reproducibility. That is a legitimate exception. Define the allowed path, ensure it contains only approved repositories, and apply the restriction at the runner or executor layer. Do not let a committed .gitmodules file choose an arbitrary local path from a shared host.
Separate fetching from executing
Fetching a submodule and running its build script are different decisions. The first can disclose data or add unreviewed content. The second can execute code. Keep submodule fetching disabled when the build does not require it. When it does, pin the submodule commit and verify that the fetched revision matches the expected record.
Use a dedicated dependency stage with minimal filesystem access and no long-lived credentials. Mount only the dependency cache and the output directory. Record the resolved URL and commit hash for each submodule. Fail the job when the URL changes unexpectedly, even if the commit hash remains the same.
Defensive baseline
Set protocol.file.allow=never for automated checkouts unless a narrowly scoped local-mirror exception is documented. Restrict external transports to the expected protocols and hosts. Do not let a repository-local configuration override the runner policy. Disable recursive submodule fetching in jobs that do not need it. Review submodule URL changes like build-system changes, and test a malicious file:// reference in an isolated runner.
Submodules are dependencies with their own source identity. Treating their transport as an automatic detail is how a repository boundary becomes a filesystem access path.
Top comments (0)