JSLuice: Extract URLs, paths, secrets, and other interesting bits from JavaScript

🧭 Overview

jsluice β€” extract URLs, paths & secrets from JavaScript

JSLuice is a command-line tool (and Go package) from BishopFox for extracting URLs, paths, secrets, and other interesting bits from JavaScript. Where most JS-parsing tools lean on flat regex and choke on minified bundles, jsluice uses tree-sitter to build an actual syntax tree of the code. That means it understands string concatenation, function calls like fetch() and document.location, object properties, and query/body parameters β€” the stuff regex silently misses.

For a web app tester, JavaScript is the single richest source of attack surface: hidden API routes, staging hostnames, hardcoded keys, GraphQL endpoints, and undocumented parameters all live in the front-end bundle. jsluice turns that pile of minified .js into clean, structured JSON you can pipe into the rest of your recon chain.

| πŸ” OffSec Tip
Think of jsluice as LinkFinder + SecretFinder rebuilt on a real parser, with JSON output as a first-class citizen. Its whole reason for existing is to be piped.


Repo: github.com/BishopFox/jsluice


πŸ“¦ Installation

jsluice is a single Go binary. You need Go installed first.

1
go install github.com/BishopFox/jsluice/cmd/jsluice@latest

Make sure your Go bin is on PATH:

1
export PATH="$PATH:$(go env GOPATH)/bin"

Verify:

1
jsluice --help

| πŸ” OffSec Tip
If jsluice: command not found after install, it’s almost always the missing GOPATH/bin on your PATH. Add the export above to your ~/.zshrc / ~/.bashrc.


πŸš€ Basic Usage

jsluice is built around modes. The general form is:

1
jsluice <mode> [flags] [file-or-url ...]

Extract URLs from a local JavaScript file:

1
jsluice urls app.js

Extract secrets from a whole directory of JS:

1
jsluice secrets ./js/*.js

You can pass local files, HTTP(S) URLs, or WARC files as arguments β€” jsluice will fetch remote URLs itself. You can also stream input on stdin (covered below).


πŸŽ›οΈ The Five Modes

Mode What it does
urls Extract URLs, paths, query params, body params, and the HTTP method/type
secrets Detect API keys, tokens, and credentials using built-in + custom patterns
tree Print the tree-sitter syntax tree of the JavaScript (great for building queries)
query Run a raw tree-sitter query against the source and print matches
format Pretty-print / reformat JavaScript source

Everything below is organized around these five modes.


🚩 All CLI Flags

🌐 Global flags (apply to every mode)

Short Long Default Description
-c --concurrency 1 Number of files to process concurrently
-C --cookie Cookie(s) to use when making HTTP requests
-H --header Header(s) to use when making HTTP requests
-P --placeholder EXPR Set the expression placeholder to a custom string
-j --raw-input false Read raw JavaScript source from stdin
-w --warc false Treat input as WARC files
-i --no-check-certificate false Ignore validation of server certificates
--profile false Profile CPU usage and save a cpu.pprof file
-h --help Show help

πŸ”— urls mode flags

Short Long Default Description
-S --include-source false Include the source code where the URL was found
-I --ignore-strings false Ignore matches that come from plain string literals
-R --resolve-paths Resolve relative paths against the absolute URL provided
-u --unique false Output each URL only once per input file

πŸ”‘ secrets mode flags

Short Long Default Description
-p --patterns JSON file containing user-defined secret patterns

πŸ” query mode flags

Short Long Default Description
-q --query Tree-sitter query to run, e.g. '(string) @matches'
-r --raw-output false Do not convert values to native types
-f --include-filename false Include the filename in the output
-F --format false Format source code in the output

πŸ“– Every Mode Explained

πŸ”— urls mode

The workhorse. It walks the syntax tree looking for anything URL-shaped β€” fetch(), XMLHttpRequest, document.location, window.open(), string assignments to path-like values, and more.

1
jsluice urls app.js

Each result is a JSON object:

1
2
3
4
5
6
7
8
9
{
"url": "/api/v2/users",
"queryParams": ["page", "limit"],
"bodyParams": ["name", "email"],
"method": "POST",
"type": "fetch",
"contentType": "application/json",
"filename": "app.js"
}
Field Meaning
url The extracted URL or path
queryParams Query-string parameter names jsluice saw attached to it
bodyParams Request-body parameter names (e.g. from a fetch body)
method HTTP method (GET, POST, …) when it can infer one
type How it was found β€” fetch, xmlhttprequest, location, set-attribute, etc.
contentType Content type when inferable
filename Source file/URL the match came from

| πŸ” OffSec Tip
The bodyParams and method fields are gold. They tell you how to hit an endpoint, not just that it exists β€” feed them straight into a fuzzer.


πŸ”‘ secrets mode

Scans for high-value strings: AWS keys, GCP keys, generic API tokens, JWTs, and anything you add via a custom pattern file.

1
jsluice secrets app.js

Output shape:

1
2
3
4
5
6
7
{
"kind": "AWS Access Key",
"data": { "key": "AKIA...", "secret": "..." },
"severity": "high",
"context": { "...": "surrounding object" },
"filename": "app.js"
}
Field Meaning
kind The type of secret matched
data The extracted secret value(s)
severity jsluice’s severity rating (info β†’ high)
context Nearby key/values that help you judge if it’s real
filename Where it was found

🌳 tree mode

Dumps the tree-sitter parse tree. You rarely ship this in a report, but it’s essential when you’re writing your own queries β€” run it to see the exact node types you need to target.

1
jsluice tree app.js

πŸ” query mode

Run an arbitrary tree-sitter query and get matches back. This is the β€œescape hatch” for anything the built-in modes don’t cover.

1
2
3
4
5
# Every string literal in the file
jsluice query -q '(string) @matches' app.js

# Every function call
jsluice query -q '(call_expression) @matches' app.js

🎨 format mode

Beautifies minified JS so you can actually read the context around a finding.

1
jsluice format min.js > pretty.js

πŸ§ͺ Practical Examples

Pull a single remote bundle and get its URLs β€” no manual download needed:

1
jsluice urls https://target.com/static/app.min.js

Only unique URLs, ignoring noisy string literals:

1
jsluice urls -u -I app.js

Resolve relative paths to absolute URLs (so /api/x becomes https://target.com/api/x):

1
jsluice urls -R https://target.com/ app.js

Scan an authenticated bundle by attaching a session cookie and header:

1
jsluice urls -C "session=abc123" -H "Authorization: Bearer eyJ..." https://target.com/app.js

Hunt secrets across every JS file you’ve saved:

1
jsluice secrets ./downloaded_js/*.js

🧠 Advanced Examples

⌨️ Read raw JavaScript from stdin

When you already have JS in a variable or pipe (not a file), use -j:

1
echo 'fetch("/api/secret", {method:"POST", body: JSON.stringify({token: x})})' | jsluice urls -j

🧩 Custom secret patterns

Create a JSON file describing patterns you care about (internal token formats, custom API key prefixes, etc.):

1
2
3
4
5
6
7
[
{
"name": "Internal Service Token",
"value": "(?i)svc_[a-z0-9]{32}",
"severity": "high"
}
]

Then load it:

1
jsluice secrets -p custom-patterns.json app.js

🏷️ Custom placeholder for unknown expressions

When jsluice can’t statically resolve part of a URL (e.g. a variable), it substitutes a placeholder β€” EXPR by default. Change it to something greppable:

1
jsluice urls -P 'FUZZ' app.js

Now dynamic segments show up as FUZZ, ready to hand to ffuf.

πŸ—„οΈ WARC support

If you’ve archived a crawl as WARC (from wget --warc-file, Katana, etc.), jsluice can read it directly:

1
jsluice urls -w crawl.warc.gz

πŸ”¬ Include source for triage

Attach the exact source snippet to every URL result so you can judge findings without reopening the file:

1
jsluice urls -S app.js | jq '{url, source}'

🎯 Real Pentest Workflow

End-to-end: from a domain to a fuzzable list of endpoints and a pile of leaked secrets.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 1. Collect historical + live URLs, keep only JavaScript
echo "target.com" \
| gau --subs \
| grep -iE '\.js($|\?)' \
| sort -u > js_urls.txt

# 2. Keep only the JS files that are actually live
cat js_urls.txt | httpx -silent -mc 200 > live_js.txt

# 3. Extract endpoints from every live bundle, resolved to absolute
cat live_js.txt \
| while read u; do jsluice urls -R "$u" "$u"; done \
| jq -r '.url' | sort -u > endpoints.txt

# 4. Extract secrets in parallel
cat live_js.txt | while read u; do jsluice secrets "$u"; done \
| jq -c 'select(.severity=="high")' > secrets.json

# 5. Feed discovered endpoints into a content-discovery fuzz
ffuf -w /usr/share/seclists/Discovery/Web-Content/raft-medium-words.txt \
-u https://target.com/FUZZ -mc 200,204,301,302,403

| πŸ” OffSec Tip
Always run step 4 (secrets) even on β€œboring” targets. Front-end bundles leak Google Maps keys, Firebase configs, Sentry DSNs, and staging tokens more often than you’d expect.


πŸ€– Automation Examples

A small reusable function you can drop in your ~/.bashrc:

1
2
3
4
5
6
7
8
9
10
11
jsl() {
# usage: jsl target.com
echo "$1" \
| gau --subs \
| grep -iE '\.js($|\?)' \
| httpx -silent -mc 200 \
| while read u; do
jsluice urls -R "$u" "$u"
jsluice secrets "$u"
done
}

Batch process a folder concurrently (jsluice defaults to 1 worker β€” bump it):

1
jsluice urls -c 20 ./js/*.js | jq -r '.url' | sort -u

🧾 JSON Output Explanation

Every mode emits newline-delimited JSON (NDJSON) β€” one object per line β€” which is exactly what jq wants. Nothing is pretty-printed by default, so it stays pipe-friendly. Key top-level fields per mode were covered in Every Mode Explained; the important habit is: never parse jsluice output with grep/awk when jq exists.


πŸ”§ jq Filtering Examples

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Just the URLs, deduplicated
jsluice urls app.js | jq -r '.url' | sort -u

# Only POST endpoints
jsluice urls app.js | jq -r 'select(.method=="POST") | .url'

# Endpoints that take body parameters (likely state-changing)
jsluice urls app.js | jq -c 'select(.bodyParams | length > 0) | {url, method, bodyParams}'

# Only high-severity secrets
jsluice secrets app.js | jq -c 'select(.severity=="high")'

# Build a param wordlist from everything jsluice saw
jsluice urls app.js | jq -r '.queryParams[], .bodyParams[]' | sort -u > params.txt

πŸ—‘οΈ Combine with Katana

Let Katana do the crawling (including JS parsing and headless), then let jsluice do the deep JS extraction:

1
2
3
4
5
katana -u https://target.com -jc -d 3 -silent \
| grep -iE '\.js($|\?)' \
| httpx -silent -mc 200 \
| while read u; do jsluice urls -R "$u" "$u"; done \
| jq -r '.url' | sort -u

πŸ•°οΈ Combine with gau

gau pulls historical URLs from Wayback, Common Crawl, and others β€” a great source of old, forgotten JS:

1
2
gau --subs target.com | grep -iE '\.js($|\?)' | sort -u \
| while read u; do jsluice urls "$u"; done

πŸ›οΈ Combine with waybackurls

Same idea, lighter tool:

1
2
waybackurls target.com | grep '\.js' | sort -u \
| while read u; do jsluice secrets "$u"; done

πŸ“œ Combine with subjs

subjs is purpose-built to pull JS file URLs from a list of hosts β€” a clean feeder for jsluice:

1
2
3
4
cat live_hosts.txt | subjs \
| httpx -silent -mc 200 \
| while read u; do jsluice urls -R "$u" "$u"; done \
| jq -r '.url' | sort -u > endpoints.txt

πŸ“‘ Combine with httpx

Use httpx to confirm which extracted endpoints actually respond before you waste time on them:

1
2
3
jsluice urls -R https://target.com/ app.js \
| jq -r '.url' | sort -u \
| httpx -silent -sc -title -mc 200,201,204,301,302,401,403

πŸ’₯ Combine with ffuf

Turn jsluice’s placeholder into a fuzz point:

1
2
3
4
5
6
# Anything jsluice couldn't resolve becomes FUZZ; feed those URLs to ffuf
jsluice urls -P 'FUZZ' app.js \
| jq -r 'select(.url | contains("FUZZ")) | .url' \
| while read target; do
ffuf -w wordlist.txt -u "$target" -mc 200,301,302,403
done

⚑ One-liners

1
2
3
4
5
6
7
8
# All endpoints from a live domain's JS, absolute + unique
gau --subs target.com | grep '\.js' | httpx -silent | while read u; do jsluice urls -R "$u" "$u"; done | jq -r '.url' | sort -u

# High-severity secrets only, one line
gau --subs target.com | grep '\.js' | httpx -silent | while read u; do jsluice secrets "$u"; done | jq -c 'select(.severity=="high")'

# Param wordlist harvested from every JS file
subjs < hosts.txt | httpx -silent | while read u; do jsluice urls "$u"; done | jq -r '.queryParams[]?,.bodyParams[]?' | sort -u

⚠️ Common Mistakes

  • Forgetting -R β€” without --resolve-paths, you get /api/x instead of a hittable absolute URL. Always resolve when you plan to probe.
  • Parsing with grep β€” the output is JSON. Use jq. grep '"url"' will bite you on nested objects.
  • Leaving concurrency at 1 β€” the default is one file at a time. For big batches, set -c.
  • Assuming stdin means URLs β€” plain stdin is treated as filenames/URLs, one per line. If you’re piping actual source code, you need -j.
  • Treating every secret as valid β€” jsluice reports candidates. Check context and severity before you cry β€œcritical” in a report.

πŸ’‘ Pro Tips & Performance

  • Cache your JS. Download bundles once (httpx -sr or wget) and run jsluice against local files repeatedly β€” re-fetching remote JS on every run is slow and noisy.
  • Bump -c for batches. Concurrency defaults to 1; -c 20 is a sane starting point for a few hundred files.
  • Use -I to cut noise. --ignore-strings drops matches from plain string literals, which removes a lot of false-positive β€œURLs” in analytics/config blobs.
  • Build a query library. Save your favorite jsluice query one-liners (GraphQL operations, WebSocket URLs, feature flags) β€” they’re reusable across targets.
  • format before manual review. Running jsluice format on a minified bundle makes human triage of a suspicious finding far faster.
  • Pin secrets patterns per client. Keep a custom-patterns.json with the client’s internal token formats and load it with -p on every engagement.

🏁 Conclusion

jsluice is one of those tools that quietly changes your recon depth. Because it parses JavaScript instead of grepping it, it surfaces endpoints, parameters, and secrets that flat tools walk right past β€” and because everything comes out as clean JSON, it slots into any pipeline you already run: gau / waybackurls for URL sources, subjs / katana for JS discovery, httpx for liveness, ffuf for fuzzing, and jq to glue it together.

Learn the five modes, keep a custom-patterns.json per client, and always resolve your paths. Do that and jsluice turns every front-end bundle on the target into a map of the attack surface.

| πŸ” OffSec Tip
Recon is a loop, not a step. Feed jsluice’s discovered endpoints back into your crawler and re-run β€” new JS often references more JS.