[security] Gitea < 1.17.2 bypassing URL restrictions during migration explained

Gitea 1.17.2 includes a security patch that prevents bypassing URL restrictions during the migration of a remote repository.

When using the web interface or the REST API to import / migrate a repository that exists on another forge, the URL of the remote forge is verified and rejected if not allowed. For instance if it starts with file://, contains %0c and more.

The migration then starts by asking for more information about the software project to be migrated, using this verified URL. The structure that is returned is supposed to contain an exact copy of the URL from which the migration must be done. But there is no guarantee that it does and some of the drivers implemented in Gitea may return a different URL.

For instance, when migrating a project from another Gitea instance, the Gitea migration driver calls the GetRepo function of the Gitea SDK, which returns the result of the /repos/{owner}/{repo} endpoint verbatim.

If a malicious server is setup by an adversary so that the /repos/{owner}/{repo} enpoint returns a URL designed to leak information from the server such as file:///etc/group, it should also be verified and discarded. This is the purpose of the check that was added in Gitea 1.17.2.

	// SECURITY: If the downloader is not a RepositoryRestorer then we need to recheck the CloneURL
	if _, ok := downloader.(*RepositoryRestorer); !ok {
		// Now the clone URL can be rewritten by the downloader so we must recheck
		if err := IsMigrateURLAllowed(repo.CloneURL, doer); err != nil {
			return err

		// SECURITY: Ensure that we haven't been redirected from an external to a local filesystem
		// Now we know all of these must parse
		cloneAddrURL, _ := url.Parse(opts.CloneAddr)
		cloneURL, _ := url.Parse(repo.CloneURL)

		if cloneURL.Scheme == "file" || cloneURL.Scheme == "" {
			if cloneAddrURL.Scheme != "file" && cloneAddrURL.Scheme != "" {
				return fmt.Errorf("repo info has changed from external to local filesystem")

		// We don't actually need to check the OriginalURL as it isn't used anywhere