A repository checkout can look complete while its build depends on code fetched from another repository. Git submodules store a reference to another Git repository and a path where its working tree is mounted. The superproject records the expected submodule commit, but the URL and fetch behavior come from configuration. That creates a separate source-control trust decision for every build that initializes submodules.
The risk is not limited to malicious Git syntax. A submodule URL can point to an unexpected host, redirect through an internal service, use an unencrypted protocol, or require credentials that the build worker should not expose. A recursive update can multiply those decisions across nested dependencies. The code may be pinned while the repository origin has changed.
Read the source definition
The .gitmodules file defines submodule names, paths, and URLs. Git uses that file when initializing a submodule in a superproject. The index and commit history record the submodule commit that the superproject expects. A build that fetches a commit from a different repository can still produce a valid local tree if the expected object is present, so reviewing only the checked-out files misses where the content came from.
Treat .gitmodules as security-relevant configuration. Review changes to URL, path, branch, and update policy with the same care as a build script. Reject submodule URLs that use plain HTTP, unauthenticated protocols, userinfo, local filesystem paths, or an unapproved host. If an internal mirror is required, use an explicit hostname controlled by the organization and validate its certificate.
Use relative URLs only when the resulting host and path are part of the intended repository layout. Relative URLs are convenient during a repository move, but their meaning depends on the clone URL used by the build job. A job that clones through an internal mirror may resolve a relative path differently from a developer workstation.
Limit protocol and credential scope
Git supports multiple transport protocols. Configure the build client to permit only the transports the organization approves, and reject the rest before any fetch begins. Keep credentials out of URLs. Use a credential helper, a short-lived access token, or an SSH identity with a narrowly scoped deploy key. Do not give a recursive submodule fetch a token that can write to every repository in the organization.
The build worker should not inherit a developer's SSH agent or a broad personal token. Run checkout with a dedicated identity that has read-only access to the exact projects named by the approved dependency policy. If the checkout needs a credential for only one host, bind the credential to that host. Monitor for authentication attempts to unexpected hosts.
Set a checkout timeout and a bounded network policy. A recursive fetch should fail if a submodule host is unreachable, rather than retry through an arbitrary proxy. For continuous integration, resolve the complete dependency graph before execution and record each repository URL and commit. That inventory makes an unexpected submodule visible even when the build succeeds.
Pin content and preserve provenance
The superproject commit pins a submodule commit, but the build should verify that the resolved repository and commit match the approved dependency record. Record the URL, commit, object hash, and fetch time in the build metadata. If a submodule is replaced with a vendored copy, preserve the original commit and license information so the source can still be audited.
Review nested submodules as separate dependencies. A submodule may itself include further sources, and the top-level change may not show the complete graph. Use a policy that can deny a new host or a new repository even when the commit hash is present. Avoid recursive initialization in privileged jobs unless the dependency policy explicitly requires it.
Tests should include a malicious or unexpected submodule definition in a disposable repository. Confirm that the checkout fails on a disallowed protocol, a URL outside the allowlist, a missing commit, and an unapproved nested repository. Then verify that a normal approved submodule checkout records the expected evidence and does not expose the credential in logs.
Submodules are a normal part of many source trees. The security problem appears when the fetch path is treated as an implementation detail. A build should know which repositories it will contact, which identities it will use, and which commit objects it will accept. That information belongs in the reviewed dependency policy, not only in a .gitmodules file that a contributor can change.