Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 18 additions & 8 deletions internal/librarian/golang/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (

"github.com/googleapis/librarian/internal/command"
"github.com/googleapis/librarian/internal/config"
"github.com/googleapis/librarian/internal/repometadata"
"github.com/googleapis/librarian/internal/semver"
"github.com/googleapis/librarian/internal/serviceconfig"
)
Expand Down Expand Up @@ -98,17 +99,26 @@ func Generate(ctx context.Context, library *config.Library, googleapisDir string
if err := generateInternalVersionFile(moduleRoot, library.Version); err != nil {
return err
}
for _, api := range library.APIs {
for i, api := range library.APIs {
if err := generateClientVersionFile(library, api.Path); err != nil {
return err
}
}
api, err := serviceconfig.Find(googleapisDir, library.APIs[0].Path, serviceconfig.LangGo)
if err != nil {
return err
}
if err := generateREADME(library, api, moduleRoot); err != nil {
return err
svcAPI, err := serviceconfig.Find(googleapisDir, api.Path, serviceconfig.LangGo)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The api.Path is passed to serviceconfig.Find, which uses it to construct file paths for reading service configurations. Since serviceconfig.validateAPI allows any path starting with google/cloud/, an attacker could use a path like google/cloud/../../../../etc/passwd to cause the tool to attempt to read arbitrary files from the filesystem where the tool is running.

Consider sanitizing or validating api.Path to ensure it does not contain directory traversal sequences before passing it to serviceconfig.Find.

References
  1. Inputs used in code generation, especially for constructing file paths, must be sanitized to prevent injection vulnerabilities like path traversal, even if the input source is considered trusted.

if err != nil {
return err
}
info := &repometadata.LibraryInfo{}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The info struct is being initialized as empty. This will result in an incomplete .repo-metadata.json file, missing fields like distribution_name and release_level. It should be populated with information from the library configuration.

Suggested change
info := &repometadata.LibraryInfo{}
info := &repometadata.LibraryInfo{
Name: library.Name,
ReleaseLevel: library.ReleaseLevel,
DescriptionOverride: library.DescriptionOverride,
}

outDir := filepath.Join(outdir, api.Path)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The api.Path from the library configuration is used to construct outDir via filepath.Join(outdir, api.Path) without validation. If api.Path contains directory traversal sequences (e.g., ../), an attacker could cause the tool to write .repo-metadata.json to an arbitrary location outside the intended output directory.

You should validate that the constructed outDir is within the intended outdir using a prefix check on the absolute paths, similar to the check performed for library.Name on lines 91-98.

References
  1. Inputs used in code generation, especially for constructing file paths, must be sanitized to prevent injection vulnerabilities like path traversal, even if the input source is considered trusted.

if err := repometadata.FromAPI(svcAPI, info, serviceconfig.LangGo, "", "", outDir); err != nil {
return err
}
// We only need to generate README.md at module root once.
if i != 0 {
continue
}
if err := generateREADME(library, svcAPI, moduleRoot); err != nil {
return err
}
}
if err := updateSnippetMetadata(library, outdir); err != nil {
return err
Expand Down
24 changes: 16 additions & 8 deletions internal/librarian/golang/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,7 @@ func generateInternalVersionFile(moduleDir, version string) (err error) {
}

func generateClientVersionFile(library *config.Library, apiPath string) (err error) {
version := filepath.Base(apiPath)
goAPI := findGoAPI(library, apiPath)
var clientDir string
if goAPI != nil && goAPI.ClientDirectory != "" {
clientDir = goAPI.ClientDirectory
}

dir := filepath.Join(library.Output, library.Name, clientDir, "api"+version)
dir := apiVersionPath(library, apiPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
Expand All @@ -87,6 +80,7 @@ func generateClientVersionFile(library *config.Library, apiPath string) (err err
}
t := template.Must(template.New("version").Parse(clientVersionTmpl))
pkg := library.Name
clientDir := clientDirectory(library, apiPath)
if clientDir != "" {
pkg = clientDir
}
Expand All @@ -96,6 +90,20 @@ func generateClientVersionFile(library *config.Library, apiPath string) (err err
})
}

func apiVersionPath(library *config.Library, apiPath string) string {
version := filepath.Base(apiPath)
clientDir := clientDirectory(library, apiPath)
return filepath.Join(library.Output, library.Name, clientDir, "api"+version)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The clientDir (derived from goAPI.ClientDirectory in the configuration) is used to construct the output directory for version files via filepath.Join without validation. An attacker could use directory traversal sequences in ClientDirectory to cause the tool to write version.go files to arbitrary locations on the filesystem.

Validate that the constructed path is within the intended output directory using a prefix check on the absolute paths.

References
  1. Inputs used in code generation, especially for constructing file paths, must be sanitized to prevent injection vulnerabilities like path traversal, even if the input source is considered trusted.

}

func clientDirectory(library *config.Library, apiPath string) string {
goAPI := findGoAPI(library, apiPath)
if goAPI != nil {
return goAPI.ClientDirectory
}
return ""
}

// writeLicenseHeader writes the license header as Go comments to the given file.
func writeLicenseHeader(f *os.File) error {
year := time.Now().Format("2006")
Expand Down
26 changes: 13 additions & 13 deletions internal/repometadata/repometadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@
errNoServiceConfig = errors.New("library has no service config from which to get metadata")
)

type libraryInfo struct {
descriptionOverride string
name string
releaseLevel string
type LibraryInfo struct {

Check failure on line 36 in internal/repometadata/repometadata.go

View workflow job for this annotation

GitHub Actions / test

symbol should have a godoc ("LibraryInfo") (godoclint)

Check failure on line 36 in internal/repometadata/repometadata.go

View workflow job for this annotation

GitHub Actions / test

symbol should have a godoc ("LibraryInfo") (godoclint)
DescriptionOverride string
Name string
ReleaseLevel string
}

// RepoMetadata represents the .repo-metadata.json file structure.
Expand Down Expand Up @@ -101,36 +101,36 @@
if api.ServiceConfig == "" {
return fmt.Errorf("failed to generate metadata for %s: %w", library.Name, errNoServiceConfig)
}
info := &libraryInfo{
descriptionOverride: library.DescriptionOverride,
name: library.Name,
releaseLevel: library.ReleaseLevel,
info := &LibraryInfo{
DescriptionOverride: library.DescriptionOverride,
Name: library.Name,
ReleaseLevel: library.ReleaseLevel,
}
return FromAPI(api, info, language, repo, defaultVersion, outdir)
}

// FromAPI generates the .repo-metadata.json file from a serviceconfig.API and additional library information.
func FromAPI(api *serviceconfig.API, info *libraryInfo, language, repo, defaultVersion, outputDir string) error {
func FromAPI(api *serviceconfig.API, info *LibraryInfo, language, repo, defaultVersion, outputDir string) error {
clientDocURL := buildClientDocURL(language, extractNameFromAPIID(api.ServiceName))
metadata := &RepoMetadata{
APIID: api.ServiceName,
NamePretty: cleanTitle(api.Title),
DefaultVersion: defaultVersion,
ClientDocumentation: clientDocURL,
ReleaseLevel: info.releaseLevel,
ReleaseLevel: info.ReleaseLevel,
Language: language,
LibraryType: "GAPIC_AUTO",
Repo: repo,
DistributionName: info.name,
DistributionName: info.Name,
}

metadata.ProductDocumentation = extractBaseProductURL(api.DocumentationURI)
metadata.IssueTracker = api.NewIssueURI
metadata.APIShortname = api.ShortName
metadata.Name = api.ShortName
metadata.APIDescription = api.Description
if info.descriptionOverride != "" {
metadata.APIDescription = info.descriptionOverride
if info.DescriptionOverride != "" {
metadata.APIDescription = info.DescriptionOverride
}

data, err := json.MarshalIndent(metadata, "", " ")
Expand Down
Loading