// Command jslibscan is a reference mitmux plugin - a Retire.js-style // passive scanner for known-vulnerable JavaScript library versions. // Unlike authcheck and paramminer, this one never sends anything: it // only reads response bodies already captured by ordinary proxying and // checks any JS library version string it can find against a small // built-in table of known-bad ranges, tagging a match "jslibscan:hit". // Demonstrating a purely passive plugin (subscribe, inspect, tag - no // repeat calls at all) alongside the two active ones is deliberate: it's // the simplest possible plugin shape, and the one least likely to // surprise anyone running it against traffic they can't afford to probe. // // The built-in table is a small, illustrative starting set (five // libraries, one well-known vulnerable-version threshold each) - NOT a // maintained vulnerability feed. Real Retire.js pulls from a // continuously updated JSON database with dozens of libraries and many // more precise version ranges; replicating that here would mean // committing to keep it current, which this reference plugin doesn't. // Extending libraries is adding one entry to the libraries slice below. // // Speaks the wire protocol directly, no internal/ipc import - see // plugins/authcheck's package doc for why, and PLUGINS.md for the // protocol. package main import ( "encoding/json" "flag" "fmt" "log" "net" "os" "path/filepath" "regexp" "strconv" "strings" ) type request struct { Type string `json:"type"` ID int64 `json:"id,omitempty"` TagPlugin string `json:"tag_plugin,omitempty"` Tag string `json:"tag,omitempty"` TagData string `json:"tag_data,omitempty"` } type summary struct { ID int64 `json:"id"` Source string `json:"source"` } type entryDetail struct { summary ResponseRaw []byte `json:"response_raw"` } type response struct { Type string `json:"type"` New *summary `json:"new,omitempty"` Detail *entryDetail `json:"detail,omitempty"` Error string `json:"error,omitempty"` } type client struct { conn net.Conn enc *json.Encoder dec *json.Decoder } func dial(path string) (*client, error) { conn, err := net.Dial("unix", path) if err != nil { return nil, err } return &client{conn: conn, enc: json.NewEncoder(conn), dec: json.NewDecoder(conn)}, nil } func (c *client) call(req request) (response, error) { if err := c.enc.Encode(req); err != nil { return response{}, err } var resp response if err := c.dec.Decode(&resp); err != nil { return response{}, err } if resp.Type == "error" { return response{}, fmt.Errorf("%s", resp.Error) } return resp, nil } func defaultSocketPath() string { if rt := os.Getenv("XDG_RUNTIME_DIR"); rt != "" { return filepath.Join(rt, "mitmux.sock") } cfg, err := os.UserConfigDir() if err != nil { return "mitmux.sock" } return filepath.Join(cfg, "mitmux", "mitmux.sock") } // library is one entry in the built-in illustrative table: match finds // a version string for the library (its first capture group is the // dotted version number, e.g. "3.4.1"), and any version strictly below // fixedIn is flagged. type library struct { name string match *regexp.Regexp fixedIn [3]int advice string } // The 0-to-15-character non-greedy gap between a library's name and its // version digits (rather than a fixed one-character separator) is // deliberate - confirmed directly, not assumed: real-world version // strings show up as "jQuery v1.8.3" (space-then-"v", two separator // characters, not one), "jquery-3.4.1.min.js" (filename form), and // "jquery.min.js?v=1.11.0" (cache-busting query string) alike. Kept // bounded and non-greedy rather than wide open, so it can't walk past // the library's own version into an unrelated number elsewhere on the // page. var libraries = []library{ { name: "jQuery", match: regexp.MustCompile(`(?i)jquery.{0,15}?(\d+\.\d+\.\d+)`), fixedIn: [3]int{3, 5, 0}, advice: "known cross-site scripting vulnerability in HTML parsing/prefiltering fixed in 3.5.0", }, { name: "Lodash", match: regexp.MustCompile(`(?i)lodash.{0,15}?(\d+\.\d+\.\d+)`), fixedIn: [3]int{4, 17, 21}, advice: "known prototype pollution / command injection vulnerabilities fixed in 4.17.21", }, { name: "Handlebars", match: regexp.MustCompile(`(?i)handlebars.{0,15}?(\d+\.\d+\.\d+)`), fixedIn: [3]int{4, 7, 7}, advice: "known prototype pollution vulnerability fixed in 4.7.7", }, { name: "Moment.js", match: regexp.MustCompile(`(?i)moment(?:\.js)?.{0,15}?(\d+\.\d+\.\d+)`), fixedIn: [3]int{2, 29, 4}, advice: "known path traversal / ReDoS vulnerabilities fixed in 2.29.4", }, { name: "AngularJS", match: regexp.MustCompile(`(?i)angular(?:js|\.js)?.{0,15}?(1\.\d+\.\d+)`), fixedIn: [3]int{1, 8, 3}, advice: "AngularJS 1.x reached end of life; known sandbox-escape/XSS issues, no fixes past 1.8.3", }, } type hit struct { Library string `json:"library"` VersionFound string `json:"version_found"` FirstFixedIn string `json:"first_fixed_in"` Advisory string `json:"advisory"` } func main() { socketPath := flag.String("socket", "", "daemon control socket path (default: same as mitmux itself)") pluginName := flag.String("name", "jslibscan", "name this plugin tags entries as") flag.Parse() path := *socketPath if path == "" { path = defaultSocketPath() } actor, err := dial(path) if err != nil { log.Fatalf("dial %s: %v", path, err) } defer actor.conn.Close() subConn, err := net.Dial("unix", path) if err != nil { log.Fatalf("dial %s (subscribe): %v", path, err) } defer subConn.Close() if err := json.NewEncoder(subConn).Encode(request{Type: "subscribe"}); err != nil { log.Fatalf("subscribe: %v", err) } log.Printf("jslibscan: watching live traffic on %s", path) dec := json.NewDecoder(subConn) for { var resp response if err := dec.Decode(&resp); err != nil { log.Fatalf("subscribe feed closed: %v", err) } if resp.Type != "new" || resp.New == nil { continue } // Not a correctness concern here the way it is for authcheck/ // paramminer (this plugin never calls repeat, so there's no // loop risk) - just avoids redundant work rescanning response // bodies that are probably near-identical to an original // request's, which is what most of a wordlist-driven probe's // own resends look like. if resp.New.Source != "proxy" { continue } if err := scanEntry(actor, *pluginName, resp.New.ID); err != nil { log.Printf("entry #%d: %v", resp.New.ID, err) } } } func scanEntry(c *client, pluginName string, id int64) error { resp, err := c.call(request{Type: "get", ID: id}) if err != nil { return fmt.Errorf("get: %w", err) } if resp.Detail == nil || len(resp.Detail.ResponseRaw) == 0 { return nil } body := string(resp.Detail.ResponseRaw) var hits []hit for _, lib := range libraries { m := lib.match.FindStringSubmatch(body) if m == nil { continue } found, ok := parseVersion(m[1]) if !ok || !isBelow(found, lib.fixedIn) { continue } hits = append(hits, hit{ Library: lib.name, VersionFound: m[1], FirstFixedIn: joinVersion(lib.fixedIn), Advisory: lib.advice, }) } if len(hits) == 0 { return nil } data, err := json.Marshal(hits) if err != nil { return fmt.Errorf("marshal hits: %w", err) } if _, err := c.call(request{Type: "tag_entry", ID: id, TagPlugin: pluginName, Tag: "jslibscan:hit", TagData: string(data)}); err != nil { return fmt.Errorf("tag_entry: %w", err) } names := make([]string, len(hits)) for i, h := range hits { names[i] = fmt.Sprintf("%s %s", h.Library, h.VersionFound) } log.Printf("#%d -> outdated JS librar(y/ies): %s", id, strings.Join(names, ", ")) return nil } func parseVersion(s string) ([3]int, bool) { parts := strings.SplitN(s, ".", 3) if len(parts) != 3 { return [3]int{}, false } var v [3]int for i, p := range parts { n, err := strconv.Atoi(p) if err != nil { return [3]int{}, false } v[i] = n } return v, true } func isBelow(v, fixedIn [3]int) bool { for i := 0; i < 3; i++ { if v[i] != fixedIn[i] { return v[i] < fixedIn[i] } } return false // exactly equal to the fixed version - not vulnerable } func joinVersion(v [3]int) string { return fmt.Sprintf("%d.%d.%d", v[0], v[1], v[2]) }