Skip to content
Closed
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
3 changes: 3 additions & 0 deletions plugin/scripts/login/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module semgrep/login

go 1.26.1
Binary file added plugin/scripts/login/login
Binary file not shown.
228 changes: 228 additions & 0 deletions plugin/scripts/login/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
// Standalone semgrep login program.
//
// Opens a browser for the user to authenticate with semgrep.dev, polls for the
// resulting token, validates it, and writes it to ~/.semgrep/settings.yml.
//
// Usage: go run . (or compile with go build)
package main

import (
"bytes"
"crypto/rand"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
"time"
)

const (
waitBetweenRetrySec = 6
maxRetries = 30 // ~3 minutes
)

func semgrepURL() string {
if u := os.Getenv("SEMGREP_URL"); u != "" {
return u
}
return "https://semgrep.dev"
}

func getSettingsPath() string {
if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" {
return filepath.Join(xdg, "semgrep", "settings.yml")
}
home, _ := os.UserHomeDir()
return filepath.Join(home, ".semgrep", "settings.yml")
}

// readToken extracts the api_token value from a simple YAML settings file.
// Returns "" if not found or file doesn't exist.
func readToken(path string) string {
data, err := os.ReadFile(path)
if err != nil {
return ""
}
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "api_token:") {
val := strings.TrimSpace(strings.TrimPrefix(line, "api_token:"))
// Strip optional surrounding quotes
val = strings.Trim(val, `'"`)
return val
}
}
return ""
}

// writeToken writes (or updates) api_token in the settings YAML file,
// preserving any other existing keys.
func writeToken(path, token string) error {
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
return err
}

var lines []string
if data, err := os.ReadFile(path); err == nil {
lines = strings.Split(string(data), "\n")
// Remove trailing empty element from split
if len(lines) > 0 && lines[len(lines)-1] == "" {
lines = lines[:len(lines)-1]
}
}

found := false
for i, line := range lines {
if strings.HasPrefix(strings.TrimSpace(line), "api_token:") {
lines[i] = "api_token: " + token
found = true
break
}
}
if !found {
lines = append(lines, "api_token: "+token)
}

tmp, err := os.CreateTemp(filepath.Dir(path), "settings*.yml")
if err != nil {
return err
}
tmpName := tmp.Name()
_, err = fmt.Fprintln(tmp, strings.Join(lines, "\n"))
tmp.Close()
if err != nil {
os.Remove(tmpName)
return err
}
return os.Rename(tmpName, path)
}

func validateToken(token string) bool {
if token == "" {
return false
}
req, err := http.NewRequest("GET", semgrepURL()+"/api/agent/deployments/current", nil)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Semgrep identified an issue in your code:
Server-Side-Request-Forgery (SSRF) exploits backend systems that initiate requests to third
parties.
If user input is used in constructing or sending these requests, an attacker could supply
malicious
data to force the request to other systems or modify request data to cause unwanted actions.

Ensure user input is not used directly in constructing URLs or URIs when initiating requests
to third party
systems from back end systems. Care must also be taken when constructing payloads using user
input. Where
possible restrict to known URIs or payloads. Consider using a server side map where key's are
used to return
URLs such as https://site/goto?key=1 where {key: 1, url: 'http://some.url/', key: 2, url: 'http://...'}.

If you must use user supplied input for requesting URLs, it is strongly recommended that the
HTTP client
chosen allows you to customize and block certain IP ranges at the network level. By blocking
RFC 1918
addresses or other network address ranges, you can limit the severity of a successful SSRF
attack. Care must
also be taken to block certain protocol or address formatting such as IPv6.

If you can not block address ranges at the client level, you may want to run the HTTP client
as a protected
user, or in a protected network where you can apply IP Table or firewall rules to block access
to dangerous
addresses. Finally, if none of the above protections are available, you could also run a
custom HTTP proxy
and force all requests through it to handle blocking dangerous addresses.

Example HTTP client that disallows access to loopback and RFC-1918 addresses

// IsDisallowedIP parses the ip to determine if we should allow the HTTP client to continue
func IsDisallowedIP(hostIP string) bool {
  ip := net.ParseIP(hostIP)
  return ip.IsMulticast() || ip.IsUnspecified() || ip.IsLoopback() || ip.IsPrivate()
}

// SafeTransport uses the net.Dial to connect, then if successful check if the resolved
// ip address is disallowed. We do this due to hosts such as localhost.lol being resolvable to
// potentially malicious URLs. We allow connection only for resolution purposes.
func SafeTransport(timeout time.Duration) *http.Transport {
  return &http.Transport{
    DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
      c, err := net.DialTimeout(network, addr, timeout)
      if err != nil {
        return nil, err
      }
      ip, _, _ := net.SplitHostPort(c.RemoteAddr().String())
      if IsDisallowedIP(ip) {
        return nil, errors.New("ip address is not allowed")
      }
      return c, err
    },
    DialTLS: func(network, addr string) (net.Conn, error) {
      dialer := &net.Dialer{Timeout: timeout}
      c, err := tls.DialWithDialer(dialer, network, addr, &tls.Config{})
      if err != nil {
        return nil, err
      }

      ip, _, _ := net.SplitHostPort(c.RemoteAddr().String())
      if IsDisallowedIP(ip) {
        return nil, errors.New("ip address is not allowed")
      }

      err = c.Handshake()
      if err != nil {
        return c, err
      }

      return c, c.Handshake()
    },
    TLSHandshakeTimeout: timeout,
  }
}

func httpRequest(requestUrl string) {
  const clientConnectTimeout = time.Second * 10
  httpClient := &http.Client{
    Transport: SafeTransport(clientConnectTimeout),
  }
  resp, err := httpClient.Get(requestUrl)
  if err != nil {
    log.Fatal(err)
  }
  defer resp.Body.Close()
  // work with resp
}

For more information on SSRF see OWASP:
https://owasp.org/www-community/attacks/Server_Side_Request_Forgery

Dataflow graph
flowchart LR
    classDef invis fill:white, stroke: none
    classDef default fill:#e7f5ff, color:#1c7fd6, stroke: none

    subgraph File0["<b>plugin/scripts/login/main.go</b>"]
        direction LR
        %% Source

        subgraph Source
            direction LR

            v0["<a href=https://github.com/semgrep/mcp-marketplace/blob/e66c0176b03b267cfeb16a188b0c804936e65dd9/plugin/scripts/login/main.go#L31 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 31] os.Getenv(&quot;SEMGREP_URL&quot;)</a>"]
        end
        %% Intermediate

        subgraph Traces0[Traces]
            direction TB

            v2["<a href=https://github.com/semgrep/mcp-marketplace/blob/e66c0176b03b267cfeb16a188b0c804936e65dd9/plugin/scripts/login/main.go#L31 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 31] u</a>"]

            v3["<a href=https://github.com/semgrep/mcp-marketplace/blob/e66c0176b03b267cfeb16a188b0c804936e65dd9/plugin/scripts/login/main.go#L110 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 110] semgrepURL</a>"]
        end
            v2 --> v3
        %% Sink

        subgraph Sink
            direction LR

            v1["<a href=https://github.com/semgrep/mcp-marketplace/blob/e66c0176b03b267cfeb16a188b0c804936e65dd9/plugin/scripts/login/main.go#L110 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 110] http.NewRequest(&quot;GET&quot;, semgrepURL()+&quot;/api/agent/deployments/current&quot;, nil)</a>"]
        end
    end
    %% Class Assignment
    Source:::invis
    Sink:::invis

    Traces0:::invis
    File0:::invis

    %% Connections

    Source --> Traces0
    Traces0 --> Sink


Loading

To resolve this comment:

🔧 No guidance has been designated for this issue. Fix according to your organization's approved methods.

💬 Ignore this finding

Reply with Semgrep commands to ignore this finding.

  • /fp <comment> for false positive
  • /ar <comment> for acceptable risk
  • /other <comment> for all other reasons

Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by G107-1.

You can view more details about this finding in the Semgrep AppSec Platform.

if err != nil {
return false
}
req.Header.Set("Authorization", "Bearer "+token)
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return false
}
resp.Body.Close()
return resp.StatusCode >= 200 && resp.StatusCode < 300
}

func generateUUID() string {
b := make([]byte, 16)
_, _ = rand.Read(b)
b[6] = (b[6] & 0x0f) | 0x40 // version 4
b[8] = (b[8] & 0x3f) | 0x80 // variant RFC4122
return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
}

func openBrowser(url string) {
var cmd string
var args []string
switch runtime.GOOS {
case "darwin":
cmd, args = "open", []string{url}
case "windows":
cmd, args = "cmd", []string{"/c", "start", url}
default:
cmd, args = "xdg-open", []string{url}
}
_ = exec.Command(cmd, args...).Start()
}

var hexToken = regexp.MustCompile(`^[0-9a-f]+$`)

func main() {
settingsPath := getSettingsPath()

existing := readToken(settingsPath)
if existing != "" && validateToken(existing) {
fmt.Printf("Already logged in. Token saved at %s.\n", settingsPath)
fmt.Println("Run `semgrep logout` first if you want to log in again.")
os.Exit(0)
}

sessionID := generateUUID()
loginURL := fmt.Sprintf("%s/login?cli-token=%s", semgrepURL(), sessionID)

fmt.Println("Opening browser to log in to semgrep.dev...")
fmt.Printf(" %s\n", loginURL)
openBrowser(loginURL)
fmt.Println("\nWaiting for login... (you have ~3 minutes)\n")

client := &http.Client{Timeout: 10 * time.Second}
pollURL := semgrepURL() + "/api/agent/tokens/requests"

for attempt := 0; attempt < maxRetries; attempt++ {
body, _ := json.Marshal(map[string]string{"token_request_key": sessionID})
resp, err := client.Post(pollURL, "application/json", bytes.NewReader(body))
if err != nil {
fmt.Fprintf(os.Stderr, "Semgrep login: Network error: %v\n", err)
os.Exit(2)
}

switch resp.StatusCode {
case http.StatusOK:
respBody, _ := io.ReadAll(resp.Body)
resp.Body.Close()

var result map[string]interface{}
if err := json.Unmarshal(respBody, &result); err != nil {
fmt.Fprintln(os.Stderr, "Semgrep login: Error: failed to parse server response.")
os.Exit(2)
}

token, _ := result["token"].(string)
if token == "" {
fmt.Fprintln(os.Stderr, "Semgrep login: Error: server returned 200 but no token in response.")
os.Exit(2)
}
if len(token) != 64 || !hexToken.MatchString(token) {
fmt.Fprintln(os.Stderr, "Semgrep login: Error: received token has unexpected format.")
os.Exit(2)
}

fmt.Println("Token received. Validating...")
if !validateToken(token) {
fmt.Fprintln(os.Stderr, "Semgrep login: Error: token validation failed.")
os.Exit(2)
}

if err := writeToken(settingsPath, token); err != nil {
fmt.Fprintf(os.Stderr, "Semgrep login: Error writing token: %v\n", err)
os.Exit(2)
}
fmt.Printf("Logged in. Token saved to %s.\n", settingsPath)
os.Exit(0)

case http.StatusNotFound:
resp.Body.Close()
// User hasn't completed browser login yet — keep polling.

default:
resp.Body.Close()
fmt.Fprintf(os.Stderr, "Semgrep login: Unexpected response from server: %d\n", resp.StatusCode)
os.Exit(2)
}

fmt.Printf(" Waiting... (%d/%d)\r", attempt+1, maxRetries)
time.Sleep(waitBetweenRetrySec * time.Second)
}

fmt.Fprintln(os.Stderr, "\nSemgrep login: Login timed out. Please try again.")
os.Exit(2)
}
3 changes: 3 additions & 0 deletions plugin/scripts/run-semgrep/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module semgrep/run-semgrep

go 1.26.1
Loading
Loading