From 6ec1882a77643761dd157e997e564ba9b69153bb Mon Sep 17 00:00:00 2001 From: Ilya Brin <464157+ilyabrin@users.noreply.github.com> Date: Mon, 28 Jul 2025 22:27:17 +0300 Subject: [PATCH] chore: Update workflows for improved security and Go setup; add path validation --- .github/workflows/codeql.yml | 11 +++-- .github/workflows/security.yml | 79 ++++++++++++++++++++++++++++++++++ .github/workflows/test.yml | 11 ++++- client.go | 32 +++++++++++++- resources.go | 37 ++++++++++++++-- 5 files changed, 159 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/security.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 77d3d2d..0ae686f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,11 +24,14 @@ jobs: with: languages: go - - name: build - uses: docker://golang:1.23.2-alpine3.20 + - name: Set up Go + uses: actions/setup-go@v5 with: - entrypoint: /bin/sh - args: -c "go build ." + go-version-file: go.mod + check-latest: true + + - name: Build + run: go build . - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 \ No newline at end of file diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..afd7821 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,79 @@ +name: Security Checks + +on: + push: + branches: [main, release] + pull_request: + branches: [main, release] + schedule: + - cron: "0 6 * * 1" # Weekly on Mondays + +permissions: + contents: read + security-events: write + +jobs: + govulncheck: + name: Go Vulnerability Check + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + check-latest: true + + - name: Install govulncheck + run: go install golang.org/x/vuln/cmd/govulncheck@latest + + - name: Run govulncheck + run: govulncheck ./... + + dependency-review: + name: Dependency Review + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + permissions: + contents: read + pull-requests: write + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Dependency Review + uses: actions/dependency-review-action@v4 + with: + fail-on-severity: moderate + deny-licenses: GPL-2.0, GPL-3.0 + + gosec: + name: Security Scan + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + check-latest: true + + - name: Run Gosec Security Scanner + uses: securego/gosec@v2.19.0 + with: + args: "-fmt=sarif -out=gosec.sarif ./..." + + - name: Upload SARIF file + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: gosec.sarif diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 00530e8..b2397b8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,9 +1,17 @@ on: [push, pull_request] name: Run Tests + +permissions: + contents: read + jobs: test: runs-on: ubuntu-latest + permissions: + contents: read + checks: write + pull-requests: write steps: - name: Checkout code uses: actions/checkout@v4 @@ -11,12 +19,13 @@ jobs: uses: actions/setup-go@v5 with: go-version-file: go.mod + check-latest: true - name: Run tests with coverage run: | go test -v -covermode=count -coverprofile=coverage.out - name: Coveralls uses: coverallsapp/github-action@v2.3.6 with: - github-token: ${{ secrets.github_token }} + github-token: ${{ secrets.GITHUB_TOKEN }} file: coverage.out format: golang \ No newline at end of file diff --git a/client.go b/client.go index ae50386..6d6bbc2 100644 --- a/client.go +++ b/client.go @@ -2,12 +2,14 @@ package disk import ( "context" + "crypto/tls" "encoding/json" "errors" "fmt" "io" "net/http" "os" + "strings" "time" ) @@ -64,13 +66,36 @@ func NewWithConfig(config *ClientConfig, token ...string) (*Client, error) { config = DefaultClientConfig() } + // Validate and sanitize token + sanitizedToken := strings.TrimSpace(token[0]) + if sanitizedToken == "" { + return nil, errors.New("access token cannot be empty") + } + // Initialize logger logger := NewLogger(config.Logger) + // Create HTTP client with secure TLS configuration + transport := &http.Transport{ + TLSClientConfig: &tls.Config{ + MinVersion: tls.VersionTLS12, + CipherSuites: []uint16{ + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + }, + }, + MaxIdleConns: 10, + MaxIdleConnsPerHost: 2, + IdleConnTimeout: 90 * time.Second, + } + return &Client{ - AccessToken: token[0], + AccessToken: sanitizedToken, HTTPClient: &http.Client{ - Timeout: config.DefaultTimeout, + Timeout: config.DefaultTimeout, + Transport: transport, }, Config: config, Logger: logger, @@ -121,6 +146,9 @@ func (c *Client) doRequest(ctx context.Context, method HttpMethod, resource stri if method == GET || method == DELETE { body = nil + } else if data != nil { + // Limit request body size to prevent memory exhaustion + body = io.LimitReader(data, 100*1024*1024) // 100MB limit } requestURL := API_URL + resource diff --git a/resources.go b/resources.go index 3d636ca..fc8eecb 100644 --- a/resources.go +++ b/resources.go @@ -7,9 +7,38 @@ import ( "errors" "fmt" "net/url" + "path/filepath" "strconv" + "strings" ) +// validatePath sanitizes and validates file paths to prevent path traversal attacks +func validatePath(path string) error { + if path == "" { + return errors.New("path cannot be empty") + } + + // Remove any null bytes + if strings.Contains(path, "\x00") { + return errors.New("path contains null bytes") + } + + // Clean the path to resolve any .. sequences + cleaned := filepath.Clean(path) + + // Check for path traversal attempts + if strings.Contains(cleaned, "..") { + return errors.New("path traversal detected") + } + + // Check for excessively long paths + if len(path) > 4096 { + return errors.New("path too long") + } + + return nil +} + func (c *Client) buildDeleteResourceURL(path string, permanently bool) string { query := url.Values{} query.Set("path", path) @@ -19,8 +48,8 @@ func (c *Client) buildDeleteResourceURL(path string, permanently bool) string { // todo: add *ErrorResponse to return func (c *Client) DeleteResource(ctx context.Context, path string, permanently bool) error { - if path == "" { - return errors.New("delete error: path cannot be empty") + if err := validatePath(path); err != nil { + return fmt.Errorf("delete error: %w", err) } url := c.buildDeleteResourceURL(path, permanently) @@ -40,8 +69,8 @@ func (c *Client) DeleteResource(ctx context.Context, path string, permanently bo } func (c *Client) GetMetadata(ctx context.Context, path string) (*Resource, *ErrorResponse) { - if len(path) < 1 { - return nil, &ErrorResponse{Error: "path cannot be empty"} + if err := validatePath(path); err != nil { + return nil, &ErrorResponse{Error: err.Error()} } var resource *Resource