Files
trufflehog/pkg/detectors/figmapersonalaccesstoken/figmapersonalaccesstoken.go
Shahzad Haider d31c04eaf9 Add Figma Personal Access Token v3 Detector (#4790)
* added v3 for figma pat

* fix regex pattern and tests in v3

* add verification function in a separate file

* add secretparts to results in figma detector

* Gate Figma v3 detector behind feature flag

* fix failing tests

* address review feedback on figma detector

* Add unit tests for Figma VerifyMatch

* restore comments

* verify Figma tokens by parsing the 403 response body
2026-08-06 22:35:37 +05:00

71 lines
2.1 KiB
Go

package figmapersonalaccesstoken
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
)
// VerifyMatch checks if the provided Figma token is valid by making a request to the Figma API.
//
// Tokens are passed via the X-Figma-Token header:
// https://developers.figma.com/docs/rest-api/personal-access-tokens/
//
// Figma documents 400, 403, 404, 429 and 500, and returns 403 for every
// authentication failure: https://developers.figma.com/docs/rest-api/errors/
// We are keeping 401 here for completeness, but it is not documented.
func VerifyMatch(ctx context.Context, client *http.Client, token string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.figma.com/v1/me", http.NoBody)
if err != nil {
return false, err
}
req.Header.Add("X-Figma-Token", token)
res, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()
switch res.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
return false, nil
case http.StatusForbidden:
return verify403(res.Body)
default:
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
// verify403 distinguishes a dead token from a live one. /v1/me requires the
// current_user:read scope (https://developers.figma.com/docs/rest-api/scopes/),
// so a live token issued without it is rejected with the same 403 as a revoked
// one. The error message is the only thing that separates them.
func verify403(body io.Reader) (bool, error) {
var res struct {
Err string `json:"err"`
}
if err := json.NewDecoder(body).Decode(&res); err != nil {
return false, fmt.Errorf("decoding 403 response body: %w", err)
}
switch {
case res.Err == "Invalid token":
return false, nil
case strings.HasPrefix(res.Err, "Invalid scope(s):"):
return true, nil
default:
// Reported as indeterminate rather than unverified so an unrecognised
// message surfaces instead of silently becoming a false negative.
return false, fmt.Errorf("unexpected 403 response: %q", res.Err)
}
}