146 lines
3.4 KiB
Go

package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"flag"
"fmt"
"net/http"
"net/url"
"os"
"strings"
)
type MainConfig struct {
name string
cookie string
secret string
}
func main() {
cfg := MainConfig{}
flag.StringVar(&cfg.secret, "secret", "", "The secret used to sign the cookie (same as in Express)")
flag.StringVar(&cfg.name, "name", "", "Optional: cookie name (just for display)")
flag.Usage = func() {
fmt.Fprintf(os.Stderr, `Usage: express-cookie-parse [flags]
Parses a raw browser Cookie: header string and inspects each cookie.
Also verifies Express.js signed cookie (cookie-parser) format.
Examples:
cookie-inspect "session=s:user123.J%%2BsOPk...; lang=en"
echo 'session=s:payload.sig; theme=dark' | cookie-inspector --secret "my-secret"
Flags:
`)
flag.PrintDefaults()
}
flag.Parse()
args := flag.Args()
if len(args) > 0 {
cfg.cookie = args[0]
}
if cfg.cookie == "" {
// Try reading from stdin
data, err := os.ReadFile("/dev/stdin")
if err != nil || len(data) == 0 {
fmt.Fprintln(os.Stderr, "Error: Provide cookie value via --cookie or pipe to stdin")
flag.Usage()
os.Exit(1)
}
cfg.cookie = strings.TrimSpace(string(data))
}
fmt.Println("Cookies:", cfg.cookie)
var cookies []*http.Cookie
c, err := http.ParseSetCookie(cfg.cookie)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to parse Cookie header: %v\n", err)
os.Exit(1)
}
cookies = append(cookies, c)
for _, c := range cookies {
inspectCookie(c, cfg.secret)
}
}
func inspectCookie(c *http.Cookie, secret string) {
fmt.Printf("Cookie: %s\n", c.Name)
fmt.Printf("%+#v\n\n", c)
cookieVal, err := url.QueryUnescape(c.Value)
if err != nil {
cookieVal = c.Value
}
fmt.Printf("%s\n\n", c.Value)
fmt.Printf("%s\n\n", cookieVal)
if !strings.HasPrefix(cookieVal, "s:") {
fmt.Println(" Not an express cookie (no 's:' prefix)")
return
}
fmt.Println(" looks like express.js signed format (s:payload.signature)")
payload64, sig64, err := decodeExpress(cookieVal)
if err != nil {
fmt.Printf(" Decoding express payload failed: %v\n", err)
return
}
data64 := payload64
parts := strings.Split(payload64, ".")
if len(parts) == 3 {
data64 = parts[1]
}
if data, err := base64.StdEncoding.DecodeString(data64); err != nil {
fmt.Printf(" Base64 decode failed: %v\n", err)
} else {
fmt.Printf(" Base64 decoded (std): %s\n", string(data))
}
if secret == "" {
fmt.Println(" (Verification skipped — provide --secret to check signature)")
} else {
if err := verifyExpress(payload64, sig64, secret); err != nil {
fmt.Printf(" Verification failed: %v\n", err)
return
}
fmt.Printf(" Verified payload.\n")
}
}
func decodeExpress(signed string) (string, string, error) {
withoutPrefix := signed[2:]
dotIdx := strings.LastIndex(withoutPrefix, ".")
if dotIdx == -1 {
return "", "", fmt.Errorf("missing '.' separator")
}
payload := withoutPrefix[:dotIdx]
sigReceived := withoutPrefix[dotIdx+1:]
return payload, sigReceived, nil
}
func verifyExpress(expressPayload, expressSig, secret string) error {
if secret == "" {
return fmt.Errorf("no secret provided")
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(expressPayload))
expectedSig := mac.Sum(nil)
expectedB64 := base64.RawURLEncoding.EncodeToString(expectedSig)
if !hmac.Equal([]byte(expectedB64), []byte(expressSig)) {
return fmt.Errorf("signature mismatch")
}
return nil
}