CVE-2018-12976 (Go)

In Go Doc Dot Org (gddo) through 2018-06-27, an attacker could use specially crafted <go-import> tags in packages being fetched by gddo to cause a directory traversal and remote code execution.

Weakness:
CWE-22: name: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.

Repository: https://github.com/golang/gddo

Vulnerable code:
file_path: gosrc/gosrc.go
snippet: func getDynamic(ctx context.Context, client *http.Client, importPath, etag string) (*Directory, error) {
	metaProto, im, sm, redir, err := fetchMeta(ctx, client, importPath)
	if err != nil {
		return nil, err
	}

	if im.projectRoot != importPath {
		var imRoot *importMeta
		metaProto, imRoot, _, redir, err = fetchMeta(ctx, client, im.projectRoot)
		if err != nil {
			return nil, err
		}
		if *imRoot != *im {
			return nil, NotFoundError{Message: "project root mismatch."}
		}
	}

	// clonePath is the repo URL from import meta tag, with the "scheme://" prefix removed.
	// It should be used for cloning repositories.
	// repo is the repo URL from import meta tag, with the "scheme://" prefix removed, and
	// a possible ".vcs" suffix trimmed.
	i := strings.Index(im.repo, "://")
	if i < 0 {
		return nil, NotFoundError{Message: "bad repo URL: " + im.repo}
	}
	proto := im.repo[:i]
	clonePath := im.repo[i+len("://"):]
	repo := strings.TrimSuffix(clonePath, "."+im.vcs)
	dirName := importPath[len(im.projectRoot):]

	resolvedPath := repo + dirName
	dir, err := getStatic(ctx, client, resolvedPath, etag)
	if err == errNoMatch {
		resolvedPath = repo + "." + im.vcs + dirName
		match := map[string]string{
			"dir":        dirName,
			"importPath": importPath,
			"clonePath":  clonePath,
			"repo":       repo,
			"scheme":     proto,
			"vcs":        im.vcs,
		}
		dir, err = getVCSDirFn(ctx, client, match, etag)
	}
	if err != nil || dir == nil {
		return nil, err
	}

	dir.ImportPath = importPath
	dir.ProjectRoot = im.projectRoot
	dir.ResolvedPath = resolvedPath
	dir.ProjectName = path.Base(im.projectRoot)
	if !redir {
		dir.ProjectURL = metaProto + "://" + im.projectRoot
	}

	if sm == nil {
		return dir, nil
	}

	if isHTTPURL(sm.projectURL) {
		dir.ProjectURL = sm.projectURL
	}

	if isHTTPURL(sm.dirTemplate) {
		dir.BrowseURL = replaceDir(sm.dirTemplate, dirName)
	}

	// TODO: Refactor this to be simpler, implement the go-source meta tag spec fully.
	if isHTTPURL(sm.fileTemplate) {
		fileTemplate := replaceDir(sm.fileTemplate, dirName)
		if strings.Contains(fileTemplate, "{file}") {
			cut := strings.LastIndex(fileTemplate, "{file}") + len("{file}") // Cut point is right after last {file} section.
			switch hash := strings.Index(fileTemplate, "#"); {
			case hash == -1: // If there's no '#', place cut at the end.
				cut = len(fileTemplate)
			case hash > cut: // If a '#' comes after last {file}, use it as cut point.
				cut = hash
			}
			head, tail := fileTemplate[:cut], fileTemplate[cut:]
			for _, f := range dir.Files {
				f.BrowseURL = strings.Replace(head, "{file}", f.Name, -1)
			}

			if strings.Contains(tail, "{line}") {
				s := strings.Replace(tail, "%", "%%", -1)
				s = strings.Replace(s, "{line}", "%d", 1)
				dir.LineFmt = "%s" + s
			}
		}
	}

	return dir, nil
}

Fixed code:
file_path: gosrc/gosrc.go
snippet: func getDynamic(ctx context.Context, client *http.Client, importPath, etag string) (*Directory, error) {
	metaProto, im, sm, redir, err := fetchMeta(ctx, client, importPath)
	if err != nil {
		return nil, err
	}

	if im.projectRoot != importPath {
		var imRoot *importMeta
		metaProto, imRoot, _, redir, err = fetchMeta(ctx, client, im.projectRoot)
		if err != nil {
			return nil, err
		}
		if *imRoot != *im {
			return nil, NotFoundError{Message: "project root mismatch."}
		}
	}

	// clonePath is the repo URL from import meta tag, with the "scheme://" prefix removed.
	// It should be used for cloning repositories.
	// repo is the repo URL from import meta tag, with the "scheme://" prefix removed, and
	// a possible ".vcs" suffix trimmed.
	i := strings.Index(im.repo, "://")
	if i < 0 {
		return nil, NotFoundError{Message: "bad repo URL: " + im.repo}
	}
	proto := im.repo[:i]
	clonePath := im.repo[i+len("://"):]
	repo := strings.TrimSuffix(clonePath, "."+im.vcs)
	if !IsValidRemotePath(repo) {
		return nil, fmt.Errorf("bad path from meta: %s", repo)
	}
	dirName := importPath[len(im.projectRoot):]

	resolvedPath := repo + dirName
	dir, err := getStatic(ctx, client, resolvedPath, etag)
	if err == errNoMatch {
		resolvedPath = repo + "." + im.vcs + dirName
		match := map[string]string{
			"dir":        dirName,
			"importPath": importPath,
			"clonePath":  clonePath,
			"repo":       repo,
			"scheme":     proto,
			"vcs":        im.vcs,
		}
		dir, err = getVCSDirFn(ctx, client, match, etag)
	}
	if err != nil || dir == nil {
		return nil, err
	}

	dir.ImportPath = importPath
	dir.ProjectRoot = im.projectRoot
	dir.ResolvedPath = resolvedPath
	dir.ProjectName = path.Base(im.projectRoot)
	if !redir {
		dir.ProjectURL = metaProto + "://" + im.projectRoot
	}

	if sm == nil {
		return dir, nil
	}

	if isHTTPURL(sm.projectURL) {
		dir.ProjectURL = sm.projectURL
	}

	if isHTTPURL(sm.dirTemplate) {
		dir.BrowseURL = replaceDir(sm.dirTemplate, dirName)
	}

	// TODO: Refactor this to be simpler, implement the go-source meta tag spec fully.
	if isHTTPURL(sm.fileTemplate) {
		fileTemplate := replaceDir(sm.fileTemplate, dirName)
		if strings.Contains(fileTemplate, "{file}") {
			cut := strings.LastIndex(fileTemplate, "{file}") + len("{file}") // Cut point is right after last {file} section.
			switch hash := strings.Index(fileTemplate, "#"); {
			case hash == -1: // If there's no '#', place cut at the end.
				cut = len(fileTemplate)
			case hash > cut: // If a '#' comes after last {file}, use it as cut point.
				cut = hash
			}
			head, tail := fileTemplate[:cut], fileTemplate[cut:]
			for _, f := range dir.Files {
				f.BrowseURL = strings.Replace(head, "{file}", f.Name, -1)
			}

			if strings.Contains(tail, "{line}") {
				s := strings.Replace(tail, "%", "%%", -1)
				s = strings.Replace(s, "{line}", "%d", 1)
				dir.LineFmt = "%s" + s
			}
		}
	}

	return dir, nil
}