hashicorp/terraform · error

subdirectory path %q leads outside of the module package

Error message

subdirectory path %q leads outside of the module package

What it means

Raised in parseModuleSourceRemote (internal/getmodules/moduleaddrs/source_parsing.go:173). The source is split into package + subdir (the part after '//'); if that user-supplied subdir begins with '../', it would traverse above the package root, which is forbidden. Terraform blocks this to prevent path-escape / arbitrary file access.

Source

Thrown at internal/getmodules/moduleaddrs/source_parsing.go:173

	if isModuleSourceLocal(raw) {
		return addrs.ModuleSourceRegistry{}, fmt.Errorf("can't use local directory %q as a module registry address", raw)
	}

	src, err := tfaddr.ParseModuleSource(raw)
	if err != nil {
		return nil, err
	}
	return addrs.ModuleSourceRegistry{
		Package: src.Package,
		Subdir:  src.Subdir,
	}, nil
}

func parseModuleSourceRemote(raw string) (addrs.ModuleSourceRemote, error) {
	var subDir string
	raw, subDir = SplitPackageSubdir(raw)
	if strings.HasPrefix(subDir, "../") {
		return addrs.ModuleSourceRemote{}, fmt.Errorf("subdirectory path %q leads outside of the module package", subDir)
	}

	// A remote source address is really just a go-getter address resulting
	// from go-getter's "detect" phase, which adds on the prefix specifying
	// which protocol it should use and possibly also adjusts the
	// protocol-specific part into different syntax.
	//
	// Note that for historical reasons this can potentially do network
	// requests in order to disambiguate certain address types, although
	// that's a legacy thing that is only for some specific, less-commonly-used
	// address types. Most just do local string manipulation. We should
	// aim to remove the network requests over time, if possible.
	norm, moreSubDir, err := NormalizePackageAddress(raw)
	if err != nil {
		// We must pass through the returned error directly here because
		// the getmodules package has some special error types it uses
		// for certain cases where the UI layer might want to include a
		// more helpful error message.

View on GitHub (pinned to c9def3e214)

Solutions

  1. Remove leading '../' from the subdir so it resolves inside the package root.
  2. If you need a sibling package, reference it by its own full source address rather than traversing out.
  3. Use path.Clean and assert the result stays within the package before submitting the source.

Example fix

// before
module "x" { source = "git::https://example.com/repo.git//../other-repo" }
// after
module "x" { source = "git::https://example.com/repo.git//modules/other" }
Defensive patterns

Strategy: validation

Validate before calling

// Reject a subdir that escapes the package via parent traversal.
func subdirStaysInPackage(subDir string) bool {
	if subDir == "" {
		return true
	}
	cleaned := path.Clean(subDir)
	return cleaned != ".." && !strings.HasPrefix(cleaned, "../")
}

Try / catch

remoteAddr, err := moduleaddrs.ParseModuleSourceRemote(raw)
if err != nil && strings.Contains(err.Error(), "leads outside of the module package") {
    return fmt.Errorf("subdir in %q escapes the package; use a path inside the repo root", raw)
}

Prevention

When it happens

Trigger: A remote source with an explicit subdir that climbs out of the package, e.g. git::https://example.com/repo.git//../../etc or https://example.com/repo.zip//../sibling. The leading '../' in the cleaned subdir is the trigger.

Common situations: Deliberate or accidental parent traversal in a subdir; attempts to reference a sibling package via relative paths; malicious input in a multi-tenant wrapper.

Related errors


AI-assisted analysis of hashicorp/terraform@c9def3e214 (2026-08-07). Data as JSON: /api/errors/b7610ad3326baa30. Report an issue: GitHub.