srdusr
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--PLAN.md53
-rw-r--r--PLUGINS.md25
-rw-r--r--README.md7
-rw-r--r--plugins/jslibscan/main.go285
4 files changed, 353 insertions, 17 deletions
diff --git a/PLAN.md b/PLAN.md
index 9a53d0e..014c902 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -973,7 +973,52 @@ visually in the TUI with the JSON-array tag data rendering correctly
(the first tag payload to exercise `jsoncolor.go`'s top-level-array
path outside its own unit tests).
-Next: the remaining Phase 1 plugins (Backslash Powered Scanner,
-Retire.js - no new protocol capability needed, same pattern `authcheck`
-and `paramminer` already validate), then the live-RPC protocol addition
-for JWT Editor/SAML Raider.
+### Third plugin shipped: `plugins/jslibscan`
+
+A Retire.js-style passive scanner for known-vulnerable JavaScript
+library versions - and, deliberately, the first purely passive plugin:
+subscribe, inspect a response body already captured by ordinary
+proxying, tag, nothing else. No `repeat` calls at all, unlike
+`authcheck` and `paramminer`. Worth having one of these in the
+reference set specifically because it's the shape least likely to
+surprise anyone running it against traffic they can't afford to
+actively probe - the safest possible plugin to try first.
+
+Checks any JS library version string it can find in a response body
+against a small, explicitly-illustrative built-in table (five
+libraries - jQuery, Lodash, Handlebars, Moment.js, AngularJS - one
+well-known vulnerable-version threshold each), tagging a match
+`jslibscan:hit` with the version found, the first fixed version, and a
+one-line advisory. Deliberately NOT a maintained vulnerability feed the
+way real Retire.js's continuously-updated JSON database is; documented
+as such in the plugin's own package doc, since claiming otherwise would
+be misleading. CVE numbers deliberately omitted from the advisory text
+in favor of a plain description of the vulnerability class - precise
+enough to be useful, without asserting a specific CVE identifier this
+reference implementation hasn't independently verified against.
+
+Found and fixed a real regex bug while verifying this one live rather
+than trusting it from the code: the first version matched jQuery's own
+actual banner comment ("jQuery v1.8.3") against nothing at all, because
+the separator pattern only allowed a single non-digit character between
+the library name and its version digits, and that banner has two (a
+space, then "v"). Confirmed the failure directly, then confirmed the
+fix against three real-world version-string shapes at once - banner
+comment, minified filename ("jquery-3.4.1.min.js"), and cache-busting
+query string ("jquery.min.js?v=1.11.0") - before it went back into the
+plugin, using a bounded non-greedy gap (`.{0,15}?`) between name and
+version rather than trying to enumerate every separator combination.
+
+Verified live end to end: a real daemon, a real origin serving both an
+outdated jQuery 1.8.3 banner and a current 3.7.1 one - the outdated
+file was correctly tagged with the right version and fix threshold, the
+current one correctly left alone, confirmed via the JSON-array tag data
+in the TUI. The same run also incidentally reconfirmed the regex fix
+was real: an entry captured *before* the fix (same file, same content,
+requested moments earlier against the buggy binary) sat right next to
+the correctly-tagged one in the history list, untagged.
+
+Next: the remaining Phase 1 plugin (Backslash Powered Scanner - no new
+protocol capability needed, same pattern the three shipped plugins
+already validate), then the live-RPC protocol addition for JWT
+Editor/SAML Raider.
diff --git a/PLUGINS.md b/PLUGINS.md
index d6c0ee9..69961d3 100644
--- a/PLUGINS.md
+++ b/PLUGINS.md
@@ -11,16 +11,21 @@ Go), and means a plugin can be developed and tested against the exact
same socket the TUI is already using, with `mitmux` itself open in
another terminal watching what happens in real time.
-`plugins/authcheck` and `plugins/paramminer` are real, working
-reference implementations - an Autorize-style authorization checker
-(resends a captured request with its auth header stripped, tags the
-entry if the response still succeeds) and a Param Miner-style hidden
-parameter prober (probes a small wordlist of candidate query
-parameters, tags the entry if any noticeably change the response) -
-both written to only ever exercise what's documented on this page, not
-any of mitmux's own internal Go packages, specifically so they prove
-this protocol is sufficient on its own. Worth reading alongside this
-document, or just copying as a starting point.
+`plugins/authcheck`, `plugins/paramminer`, and `plugins/jslibscan` are
+real, working reference implementations - an Autorize-style
+authorization checker (resends a captured request with its auth header
+stripped, tags the entry if the response still succeeds), a Param
+Miner-style hidden parameter prober (probes a small wordlist of
+candidate query parameters, tags the entry if any noticeably change the
+response), and a Retire.js-style passive scanner for known-vulnerable
+JS library versions (reads response bodies already captured by ordinary
+proxying, no probing at all) - all written to only ever exercise what's
+documented on this page, not any of mitmux's own internal Go packages,
+specifically so they prove this protocol is sufficient on its own.
+Worth reading alongside this document, or just copying as a starting
+point. `jslibscan` in particular is the simplest possible plugin shape
+- subscribe, inspect, tag, nothing else - worth starting from if a
+plugin idea doesn't need to send any traffic of its own.
## Connecting
diff --git a/README.md b/README.md
index b23ba4e..4c73472 100644
--- a/README.md
+++ b/README.md
@@ -70,9 +70,10 @@ the same socket the TUI itself uses - see [`PLUGINS.md`](PLUGINS.md).
a badge in the history list, searchable via `tag:name`, viewable
(`T` from detail view) with JSON data syntax-highlighted the same way
a pretty-printed response is. See [`PLUGINS.md`](PLUGINS.md) and
- `plugins/authcheck`/`plugins/paramminer` for real, working ones (an
- Autorize-style authorization checker, a Param Miner-style hidden
- parameter prober).
+ `plugins/authcheck`/`plugins/paramminer`/`plugins/jslibscan` for
+ real, working ones (an Autorize-style authorization checker, a Param
+ Miner-style hidden parameter prober, a Retire.js-style scanner for
+ known-vulnerable JS library versions).
- **Comparer**: mark one entry (`c`), then `c` on a different entry to
see a colored unified diff of either side's request or response.
- **Decoder**: standalone URL/Base64/Hex/HTML encode and decode (`d`),
diff --git a/plugins/jslibscan/main.go b/plugins/jslibscan/main.go
new file mode 100644
index 0000000..0bbcc94
--- /dev/null
+++ b/plugins/jslibscan/main.go
@@ -0,0 +1,285 @@
+// 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])
+}