mirror of
https://github.com/BishopFox/sj.git
synced 2026-11-07 17:08:08 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90e9c78d9a | ||
|
|
3f99ffb48c | ||
|
|
331bb1a05c | ||
|
|
3c2a3b807d | ||
|
|
7ef8abdb8e | ||
|
|
f1f9e19422 | ||
|
|
0d6776b758 | ||
|
|
8f4e4a5d9f |
No files matched your search
@@ -30,6 +30,24 @@ archives:
|
||||
- goos: windows
|
||||
format: zip
|
||||
|
||||
dockers_v2:
|
||||
- images:
|
||||
- "ghcr.io/bishopfox/sj"
|
||||
dockerfile: Dockerfile
|
||||
platforms:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
tags:
|
||||
- "{{ .Version }}"
|
||||
- "latest"
|
||||
labels:
|
||||
org.opencontainers.image.title: "{{ .ProjectName }}"
|
||||
org.opencontainers.image.description: "Swagger Jacker"
|
||||
org.opencontainers.image.version: "{{ .Version }}"
|
||||
org.opencontainers.image.revision: "{{ .FullCommit }}"
|
||||
org.opencontainers.image.source: "https://github.com/BishopFox/sj"
|
||||
org.opencontainers.image.licenses: "MIT"
|
||||
|
||||
changelog:
|
||||
disable: false
|
||||
sort: asc
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
FROM gcr.io/distroless/static-debian12:nonroot
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
COPY ${TARGETOS}/${TARGETARCH}/sj /usr/local/bin/sj
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/sj"]
|
||||
+56
-18
@@ -52,12 +52,13 @@ var bruteCmd = &cobra.Command{
|
||||
printWarn("Error parsing URL: %s", err)
|
||||
}
|
||||
target := u.Scheme + "://" + u.Host
|
||||
normalizedBasePath := normalizeBasePath(basePath)
|
||||
if endpointWordlist == "" {
|
||||
allURLs = append(allURLs, makeURLs(target, priorityURLs, "", true)...)
|
||||
allURLs = append(allURLs, makeURLs(target, jsonEndpoints, "", false)...)
|
||||
allURLs = append(allURLs, makeURLs(target, javascriptEndpoints, ".js", false)...)
|
||||
allURLs = append(allURLs, makeURLs(target, jsonEndpoints, ".json", false)...)
|
||||
allURLs = append(allURLs, makeURLs(target, jsonEndpoints, "/", false)...)
|
||||
allURLs = append(allURLs, makeURLs(target, normalizedBasePath, priorityURLs, "", true)...)
|
||||
allURLs = append(allURLs, makeURLs(target, normalizedBasePath, jsonEndpoints, "", false)...)
|
||||
allURLs = append(allURLs, makeURLs(target, normalizedBasePath, javascriptEndpoints, ".js", false)...)
|
||||
allURLs = append(allURLs, makeURLs(target, normalizedBasePath, jsonEndpoints, ".json", false)...)
|
||||
allURLs = append(allURLs, makeURLs(target, normalizedBasePath, jsonEndpoints, "/", false)...)
|
||||
} else {
|
||||
endpointList, err := os.Open(endpointWordlist)
|
||||
if err != nil {
|
||||
@@ -68,7 +69,7 @@ var bruteCmd = &cobra.Command{
|
||||
scanner := bufio.NewScanner(endpointList)
|
||||
for scanner.Scan() {
|
||||
endpoint := scanner.Text()
|
||||
fullURL := target + endpoint
|
||||
fullURL := target + normalizedBasePath + endpoint
|
||||
allURLs = append(allURLs, fullURL)
|
||||
}
|
||||
|
||||
@@ -119,7 +120,7 @@ var bruteCmd = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
func makeURLs(target string, endpoints []string, fileExtension string, skipPrefix bool) []string {
|
||||
func makeURLs(target string, basePath string, endpoints []string, fileExtension string, skipPrefix bool) []string {
|
||||
urls := []string{}
|
||||
if !skipPrefix {
|
||||
for _, dir := range prefixDirs {
|
||||
@@ -127,7 +128,7 @@ func makeURLs(target string, endpoints []string, fileExtension string, skipPrefi
|
||||
if dir == "" && endpoint == "" {
|
||||
continue
|
||||
}
|
||||
targetURL := target + dir + endpoint + fileExtension
|
||||
targetURL := target + basePath + dir + endpoint + fileExtension
|
||||
urls = append(urls, targetURL)
|
||||
|
||||
}
|
||||
@@ -137,7 +138,7 @@ func makeURLs(target string, endpoints []string, fileExtension string, skipPrefi
|
||||
if endpoint == "" {
|
||||
continue
|
||||
}
|
||||
targetURL := target + endpoint + fileExtension
|
||||
targetURL := target + basePath + endpoint + fileExtension
|
||||
urls = append(urls, targetURL)
|
||||
}
|
||||
}
|
||||
@@ -159,16 +160,10 @@ func findDefinitionFile(urls []string, client http.Client) (bool, *openapi3.T) {
|
||||
}
|
||||
}
|
||||
} else if strings.Contains(ct, "application/javascript") {
|
||||
bodyBytes, bodyString, _ := MakeRequest(client, "GET", url, timeout, nil)
|
||||
bodyBytes, _, _ := MakeRequest(client, "GET", url, timeout, nil)
|
||||
if bodyBytes != nil {
|
||||
regexPattern := regexp.MustCompile(`(?s)let\s+(\w+)\s*=\s*({.*?});`)
|
||||
matches := regexPattern.FindAllStringSubmatch(bodyString, -1)
|
||||
for _, match := range matches {
|
||||
if len(match) < 3 {
|
||||
continue
|
||||
}
|
||||
jsonContent := match[2]
|
||||
checkSpec := UnmarshalSpec([]byte(jsonContent))
|
||||
if jsonContent, ok := ExtractJSONFromJSSpec(bodyBytes); ok {
|
||||
checkSpec := UnmarshalSpec(jsonContent)
|
||||
if strings.HasPrefix(checkSpec.OpenAPI, "2") || strings.HasPrefix(checkSpec.OpenAPI, "3") {
|
||||
printInfo("\nFound operation definitions embedded in JavaScript file at %s\n", url)
|
||||
return true, checkSpec
|
||||
@@ -191,6 +186,49 @@ func init() {
|
||||
bruteCmd.Flags().BoolVarP(&endpointOnly, "endpoint-only", "e", false, "Only return the identified endpoint")
|
||||
}
|
||||
|
||||
// ExtractJSONFromJSSpec scans a Swagger-UI-style JavaScript bundle for an
|
||||
// OpenAPI/Swagger document assigned to a top-level var/let/const. If the
|
||||
// captured value is itself a wrapper object (swagger-ui-express / swagger-ui
|
||||
// bundle config), it unwraps one level via the swaggerDoc/spec/openapi keys.
|
||||
// Returns the extracted JSON bytes and true on success, or the original
|
||||
// bytes and false on failure.
|
||||
func ExtractJSONFromJSSpec(bodyBytes []byte) ([]byte, bool) {
|
||||
re := regexp.MustCompile(`(?s)(?:let|const|var)\s+(\w+)\s*=\s*({.*?});`)
|
||||
matches := re.FindAllStringSubmatch(string(bodyBytes), -1)
|
||||
for _, m := range matches {
|
||||
if len(m) < 3 {
|
||||
continue
|
||||
}
|
||||
candidate := []byte(m[2])
|
||||
if looksLikeAPISpec(candidate) {
|
||||
return candidate, true
|
||||
}
|
||||
var wrapper map[string]json.RawMessage
|
||||
if err := json.Unmarshal(candidate, &wrapper); err != nil {
|
||||
continue
|
||||
}
|
||||
for _, key := range []string{"swaggerDoc", "spec", "openapi"} {
|
||||
if inner, ok := wrapper[key]; ok && looksLikeAPISpec(inner) {
|
||||
return inner, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return bodyBytes, false
|
||||
}
|
||||
|
||||
func looksLikeAPISpec(b []byte) bool {
|
||||
var probe struct {
|
||||
OpenAPI string `json:"openapi"`
|
||||
Swagger string `json:"swagger"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &probe); err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.HasPrefix(probe.OpenAPI, "3") ||
|
||||
strings.HasPrefix(probe.OpenAPI, "2") ||
|
||||
strings.HasPrefix(probe.Swagger, "2")
|
||||
}
|
||||
|
||||
func ExtractSpecFromJS(bodyBytes []byte) []byte {
|
||||
var openApiIndex int
|
||||
var specClose int
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ $ sj convert -u https://petstore.swagger.io/v2/swagger.json -o openapi.json`,
|
||||
printErr("Command not specified. See the --help flag for usage.")
|
||||
}
|
||||
},
|
||||
Version: "2.6.0",
|
||||
Version: "2.7.0",
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
|
||||
+76
-6
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -49,6 +50,19 @@ func BuildRequestsFromPaths(spec map[string]interface{}, client http.Client, rep
|
||||
}
|
||||
slices.Sort(pathKeys)
|
||||
|
||||
// Snapshot CLI-supplied headers so per-endpoint Content-Type
|
||||
// mutations from EnforceSingleContentType don't leak across endpoints.
|
||||
userHeaders := append([]string(nil), Headers...)
|
||||
var userContentType string
|
||||
for _, h := range userHeaders {
|
||||
if strings.HasPrefix(strings.ToLower(h), "content-type:") {
|
||||
if idx := strings.Index(h, ":"); idx >= 0 {
|
||||
userContentType = strings.TrimSpace(h[idx+1:])
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var errorDescriptions = make(map[any]string)
|
||||
for _, pathName := range pathKeys {
|
||||
pathItem := paths[pathName]
|
||||
@@ -68,6 +82,11 @@ func BuildRequestsFromPaths(spec map[string]interface{}, client http.Client, rep
|
||||
continue
|
||||
default:
|
||||
if opMap, ok := op.(map[string]interface{}); ok {
|
||||
// Reset header state so the previous endpoint's
|
||||
// Content-Type doesn't leak into this one.
|
||||
Headers = append([]string(nil), userHeaders...)
|
||||
contentType = userContentType
|
||||
|
||||
if responses, ok := opMap["responses"].(map[string]interface{}); ok {
|
||||
for status, respItem := range responses {
|
||||
if respMap, ok := respItem.(map[string]interface{}); ok {
|
||||
@@ -225,23 +244,42 @@ func BuildRequestsFromPaths(spec map[string]interface{}, client http.Client, rep
|
||||
if cType == "application/json" {
|
||||
bodyBytes, err := json.Marshal(example)
|
||||
if err == nil {
|
||||
bodyData = string(bodyBytes)
|
||||
curl += fmt.Sprintf(" -H \"Content-Type: application/json\" -d '%s'", bodyBytes)
|
||||
}
|
||||
}
|
||||
if cType == "application/xml" || cType == "text/xml" {
|
||||
if obj, ok := example.(map[string]interface{}); ok {
|
||||
xml := XmlFromObject(obj)
|
||||
bodyData = xml
|
||||
curl += fmt.Sprintf(" -H \"Content-Type: %s\" -d '%s'", cType, xml)
|
||||
}
|
||||
}
|
||||
if cType == "application/x-www-form-urlencoded" || cType == "multipart/form-data" {
|
||||
if cType == "application/x-www-form-urlencoded" {
|
||||
if obj, ok := example.(map[string]interface{}); ok {
|
||||
var formParts []string
|
||||
for k, v := range obj {
|
||||
formParts = append(formParts, fmt.Sprintf("%s=%v", k, v))
|
||||
}
|
||||
formData := strings.Join(formParts, "&")
|
||||
curl += fmt.Sprintf(" -H \"Content-Type: %s\" -d '%s'", cType, formData)
|
||||
bodyData = strings.Join(formParts, "&")
|
||||
curl += fmt.Sprintf(" -H \"Content-Type: %s\" -d '%s'", cType, bodyData)
|
||||
}
|
||||
}
|
||||
if cType == "multipart/form-data" {
|
||||
if obj, ok := example.(map[string]interface{}); ok {
|
||||
var buf bytes.Buffer
|
||||
mw := multipart.NewWriter(&buf)
|
||||
for k, v := range obj {
|
||||
_ = mw.WriteField(k, fmt.Sprintf("%v", v))
|
||||
}
|
||||
_ = mw.Close()
|
||||
bodyData = buf.String()
|
||||
// Replace the bare multipart/form-data Content-Type
|
||||
// (set earlier in the loop) with a boundary-aware one.
|
||||
EnforceSingleContentType(mw.FormDataContentType())
|
||||
for k, v := range obj {
|
||||
curl += fmt.Sprintf(" -F \"%s=%v\"", k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -265,9 +303,8 @@ func BuildRequestsFromPaths(spec map[string]interface{}, client http.Client, rep
|
||||
switch os.Args[1] {
|
||||
case "automate":
|
||||
var postBodyData string
|
||||
if strings.ToLower(method) == "post" && strings.Contains(curl, "-d") {
|
||||
dataIndex := strings.Index(curl, "'")
|
||||
postBodyData = curl[dataIndex+1 : len(curl)-1]
|
||||
if strings.ToLower(method) == "post" {
|
||||
postBodyData = bodyData
|
||||
}
|
||||
|
||||
_, resp, sc := MakeRequest(client, strings.ToUpper(method), targetURL, timeout, bytes.NewReader([]byte(postBodyData)))
|
||||
@@ -540,6 +577,15 @@ func GenerateExample(node *SchemaNode) interface{} {
|
||||
}
|
||||
|
||||
func GenerateRequests(bodyBytes []byte, client http.Client, replayClient *http.Client) {
|
||||
// Swagger UI bundles serve the spec wrapped in JavaScript (e.g.
|
||||
// swagger-ui-init.js). Detect that and unwrap before parsing so the
|
||||
// downstream YAML/JSON unmarshal succeeds.
|
||||
if looksLikeJSSpec(bodyBytes) {
|
||||
if extracted, ok := ExtractJSONFromJSSpec(bodyBytes); ok {
|
||||
bodyBytes = extracted
|
||||
}
|
||||
}
|
||||
|
||||
// Ingests the specification file
|
||||
spec := SafelyUnmarshalSpec(bodyBytes)
|
||||
|
||||
@@ -807,6 +853,30 @@ func SetScheme(swaggerURL string) (scheme string) {
|
||||
return scheme
|
||||
}
|
||||
|
||||
// looksLikeJSSpec is a cheap content sniff so we only run the JS-extraction
|
||||
// regex against bodies that plausibly contain JavaScript. Normal JSON
|
||||
// (starts with `{`/`[`) and YAML (starts with `openapi:`/`swagger:`/`---`/`#`)
|
||||
// fall through unchanged.
|
||||
func looksLikeJSSpec(b []byte) bool {
|
||||
if strings.HasSuffix(strings.ToLower(swaggerURL), ".js") ||
|
||||
strings.HasSuffix(strings.ToLower(localFile), ".js") ||
|
||||
strings.ToLower(format) == "js" {
|
||||
return true
|
||||
}
|
||||
trimmed := bytes.TrimLeft(b, " \t\r\n")
|
||||
prefixes := [][]byte{
|
||||
[]byte("var "), []byte("let "), []byte("const "),
|
||||
[]byte("(function"), []byte("window."),
|
||||
[]byte("//"), []byte("/*"),
|
||||
}
|
||||
for _, p := range prefixes {
|
||||
if bytes.HasPrefix(trimmed, p) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func SafelyUnmarshalSpec(data []byte) map[string]interface{} {
|
||||
|
||||
var doc map[string]interface{}
|
||||
|
||||
Reference in new issue
Block a user