Bishop Fox named “Leader” in 2024 GigaOm Radar for Attack Surface Management. Read the Report ›

Introducing jsluice: A Technical Deep-Dive for JavaScript Gold (Part 2)

Dark black and purple background with turquoise, purple, and white letters. A turquoise gold mining pan with gold chunks above a purple gold mining cart filled with chunks of gold.

Share

A sluice box is a box lined with riffles or ridges. When you put a sluice box in flowing water that contains little bits of gold, the heavy gold gets stuck in the riffles for you to easily collect, without having to manually sift through tons of dirt and silt.

This is what jsluice attempts to do for JavaScript - run megabytes of mostly junk though it and get just the interesting bits spat back out at you. There are four modes in jsluice: urls, secrets, tree, and query. jsluice accepts a list of files either as command line arguments or one per line fed into its stdin. This means you can either run something like this:

jsluice urls fetch.js

Or like this:

find . -name '*.js' | jsluice urls

URLs

Let's go back to that slightly more complicated fetch example we used before and see how the urls mode deals with it:

fetch('/api/v2/guestbook', {
  method: "POST",
  headers: {
    "Content-Type": "application/json"
  },
  body: JSON.stringify({msg: "..."})
})

I've saved that example to a file called fetch.js. jsluice outputs the JSONL format, i.e. one JSON object per line, so I've piped it to jq to make things a little easier to read:

▶ jsluice urls fetch.js | jq
{
  "url": "/api/v2/guestbook",
  "queryParams": [],
  "bodyParams": [],
  "method": "POST",
  "headers": {
    "Content-Type": "application/json"
  },
  "contentType": "application/json",
  "type": "fetch"
}

So jsluice managed to extract the path, the HTTP method, and the headers. It also labelled the 'type' as 'fetch', so we know where it was extracted from. It didn't extract the body of the request in this case, but nobody's perfect. You could probably make it extract the body too with a bit of work, but in my analysis of several gigabytes of JavaScript gathered from around the web, I found that in most cases the body field is populated with just a variable that we can't easily know the value of anyway.

Let's look at another, slightly more challenging example: XMLHttpRequest:

function callAPI(method, callback){
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = callback;
 
    xhr.open('GET', '/api/' + method + '?format=json', true);
    xhr.setRequestHeader('Accept', 'application/json');
 
    if (window.env != 'prod'){
        xhr.setRequestHeader('X-Env', 'staging')
    }
 
    xhr.send();
}

The problem with code that uses XMLHttpRequest, for us at least, is that the data we want is spread out between different method-calls. The HTTP method and path are in the call to open, and headers are added using the setRequestHeader method. One of the calls to add a request header is inside a conditional, further complicating things.

Let's see how jsluice does:

▶ jsluice urls xhr.js | jq
{
  "url": "/api/EXPR?format=json",
  "queryParams": [
    "format"
  ],
  "bodyParams": [],
  "method": "GET",
  "headers": {
    "Accept": "application/json",
    "X-Env": "staging"
  },
  "type": "XMLHttpRequest.open"
}

We managed to extract the path complete with query string, the HTTP method, and the headers even though one of those headers was inside a conditional. To extract the headers, jsluice is doing something that would be difficult without a syntax tree. The flow looks something like:

  • Look for .open() calls with at least two arguments
  • Check the first argument is a valid HTTP method
  • Climb the syntax tree to find the containing scope – usually a function definition
  • Look for calls to .setRequestHeader() only within that scope, on an object of the same name

Neat!

The path it extracted looks a bit funky though because it has EXPR in the middle of it. The path had a variable called method concatenated to the end of it, and then a query string concatenated to that:

xhr.open('GET', '/api/' + method + '?format=json', true);

This is exactly the kind of scenario where regular expressions can falter. They might miss the path entirely, only capture the first part, only capture the query string, or maybe just include the quotes and plus signs in their output. None of these options are great. If we were doing dynamic analysis with a real JavaScript engine we could get the value of the method variable, but only if the function was executed.

The static analysis performed by jsluice collapses these concatenations, replacing any expression with the EXPR string. This isn't perfect either, and occasionally produces not-so-useful results. However, the result is usually able to be parsed by a URL parser and makes it clear which part of the URL is variable. You might want to use this part of the URL as a place to inject items from a word list.

If you're not happy with EXPR being the replacement string, you can change it with the --pattern command-line flag, or if you're using the jsluice package directly you can set jsluice.ExpressionPlaceholder to something else. Perhaps the string 'FUZZ' would be a good choice if you're planning on passing the URLs to ffuf.

jsluice can find URLs, paths, and other request data used in:

  • Assignments to document.location, val.href, val.src, etc
  • Calls to location.replace, window.open, and fetch
  • Uses of XMLHttpRequest
  • Calls to jQuery's $.get, $.post, and $.ajax
  • Any other string literal that contains something that looks like a URL

You will sometimes get duplicate matches from that last one, but there's an option, --ignore-strings, to disable the feature if you find that to be a problem.

Secrets

URLs and paths aren't the only gold to be found in JavaScript; sometimes there are secrets too. One of the most damaging things we've come across are AWS access keys and their associated secrets. Here's an example object that contains an example key and secret:

var config = {
    bucket: "examplebucket",
    awsKey: "AKIAIOSFODNN7EXAMPLE",
    awsSecret: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
    server: "someserver.example.com"
}

AWS access keys have a nice property that they have a fixed set of prefixes: AKIA, ASIA, AGPA and so on. You can see a full list on this page if you're interested. That makes them easy to write regular expressions for, so why bother adding a feature to jsluice to extract them?

The first reason is that by using the syntax tree to extract string literals, we don't have to deal with the different kinds of quotes, and that makes writing regular expressions for them easier and more reliable. The second reason is context.

An AWS key by itself can be somewhat interesting, but it's only really damaging if paired with an associated secret. Unlike the key, the secret does not have a common prefix: it's just a block of Base64-encoded data. You can write a regular expression for that, but you will inadvertently match all sorts of other things, and your signal-to-noise ratio will be terrible. Here's what jsluice's secrets mode does with the above example:

▶ jsluice secrets awskey.js | jq
{
  "kind": "AWSAccessKey",
  "data": {
    "key": "AKIAIOSFODNN7EXAMPLE",
    "secret": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
  },
  "filename": "awskey.js",
  "severity": "high",
  "context": {
    "awsKey": "AKIAIOSFODNN7EXAMPLE",
    "awsSecret": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
    "bucket": "examplebucket",
    "server": "someserver.example.com"
  }
}

There are a couple of important things to note here. The first of which is that both the key and the secret were extracted and put into the data field with predictable key names. If you wanted to pass this data off to another stage of your automation that checks the validity of the key and secret, you can do that!

Secondly, the entire object in which the key and secret were found is included in the context field. There's often relevant information stored alongside credentials; information that can be of great help to a human reviewing these findings. In this example, the AWS key and secret might be for writing to an S3 bucket called examplebucket, which might serve its content through someserver.example.com.

jsluice has built-in detection of keys and secrets for AWS, GCP, and GitHub. There are more secret types to be found than I care to count though, which is why you can also provide your own custom patterns for matching secrets.

Custom Secret Patterns

The jsluice command-line tool lets you provide a JSON file using the --patterns or the -p flag that defines a list of user-defined patterns for matching secrets. Here's a small example patterns file:

[
  {
    "name": "base64",
    "value": "(eyJ|YTo|Tzo|PD[89]|rO0)[%a-zA-Z0-9+/]+={0,2}",
    "severity": "low"
  },
  {
    "name": "genericSecret",
    "key": "(secret|private|key)",
    "value": "[%a-zA-Z0-9+/]+"
  }
]

Each pattern has a name that is used in the kind field in the tool's output. There are two additional fields: key and value. The value field contains a Go-format regular expression that will be run against all string literals in the JavaScript source code. The quotes will be stripped off, so you don't have to worry about them. The key field contains a regular expression that will be run against the key names in JavaScript objects. If you specify both fields, both regular expressions will need to match for a result to be returned.

The severity field lets you categorize your patterns for later prioritization. You probably care more about finding an API key and secret than some Base64-encoded JSON after all.

Here's some, admittedly silly, example code for us to try the above patterns on:

function getConfig(){
    let config = {
        randomStr: "abc123xyz256",
        secret: "I quite like PHP",
    }
    return "eyJsb2wiOiAic29tZSBKU09OISIsICJjb3VudCI6IDEyM30K"
}

When we run jsluice in the secrets mode and provide the patterns file, we get this:

▶ jsluice secrets -p patterns.json b64.js | jq
{
  "kind": "base64",
  "data": {
    "match": "eyJsb2wiOiAic29tZSBKU09OISIsICJjb3VudCI6IDEyM30K"
  },
  "filename": "b64.js",
  "severity": "low",
  "context": null
}
{
  "kind": "genericSecret",
  "data": {
    "key": "secret",
    "value": "I quite like PHP"
  },
  "filename": "b64.js",
  "severity": "info",
  "context": {
    "randomStr": "abc123xyz256",
    "secret": "I quite like PHP"
  }
}

Our base64 pattern seemed to work fine, but the genericSecret pattern matched a different kind of secret to the kind we were really hoping for. That's because the regular expression matched part of the value. If you want to stop this kind of thing from happening, you can add anchors to the regular expression. So, this:

[%a-zA-Z0-9+/]+

becomes this:

^[%a-zA-Z0-9+/]+$

And will now match only if the entire value conforms to the regular expression.

Matching Objects

Earlier we used an example of an AWS key and a secret that were in the same object. There are likely to be other situations where you want to match against more than one thing in an object, so the custom patterns support that too! One thing we've come across a few times that are occasionally interesting, are configuration objects for Firebase. They look distinctive; something like this:

let fbConfig = {
    apiKey: "AIzaSyB47WKzDu9kkmFAsAYFlagkuJxdEXAMPLE",
    authDomain: "someauthdomain.firebaseapp.com",
    projectId: "someprojectid",
    storageBucket: "somebucketthatisnotthere.appspot.com",
    messagingSenderId: "586572527435",
    appId: "1:588572526435:web:14c624659103dc3e74b755"
}

The object field in a pattern can be provided as a list of patterns to match against object keys and/or values. If we wanted to match objects like the one above, we might use a pattern like this one:

{
    "name": "firebaseConfig",
    "severity": "medium",
    "object": [
        {"key": "apiKey", "value": "^AIza.+"},
        {"key": "authDomain"},
        {"key": "projectId"},
        {"key": "storageBucket"}
    ]
}

You could add more regular expressions for the values, and make them more specific if you like, but this would be a good starting point.

jsluice will provide the entire object that was matched in the data field. The context field will be set to null because there's no further context to provide:

▶ jsluice secrets -p patterns.json firebase.js | jq
{
  "kind": "firebaseConfig",
  "data": {
    "apiKey": "AIzaSyB47WKzDu9kkmFAsAYFlagkuJxdEXAMPLE",
    "appId": "1:588572526435:web:14c624659103dc3e74b755",
    "authDomain": "someauthdomain.firebaseapp.com",
    "messagingSenderId": "586572527435",
    "projectId": "someprojectid",
    "storageBucket": "somebucketthatisnotthere.appspot.com"
  },
  "filename": "firebase.js",
  "severity": "medium",
  "context": null
}

That's everything you can do with custom patterns. If there's more functionality in this area you'd like to see, let us know! If you want to do anything more complicated in the meantime, you can always dig into the code and write your own matchers with the full power of Go and Tree-sitter at your fingertips.

Trees and Queries

We've already seen jsluice's tree mode in action, but here's a refresher: it prints a textual representation of the syntax tree for any JavaScript file, like this:

▶ cat hello.js
console.log("Hello, world!")
▶ jsluice tree hello.js
hello.js:
program
  expression_statement
    call_expression
      function: member_expression
        object: identifier (console)
        property: property_identifier (log)
      arguments: arguments
        string ("Hello, world!")

Now, this is interesting for sure, at least if you're the kind of person who likes syntax trees. It is useful if you want to use jsluice's other mode though: the query mode.

The query mode lets you run raw Tree-sitter queries against JavaScript files. Now, the Tree-sitter query syntax is a little tricky, and there's some quirks you need to be aware of, but it can be useful for doing analysis on a whole bunch of JavaScript files. We won't cover the full syntax here, but we will look at a few examples to give you a flavor of what's possible. Let's run some queries on the XMLHttpRequest example code from earlier in this post.

First up, probably just about the simplest thing you could do is extract all the string literals:

▶ jsluice query -q '(string) @match' xhr.js
"GET"
"/api/"
"?format=json"
"Accept"
"application/json"
"prod"
"X-Env"
"staging"

JSON is the default output format, so jsluice parsed the strings found in the JavaScript and then re-encoded them using JSON rules. This means that escape sequences like \x20 that are valid in JavaScript but not JSON are interpreted correctly:

▶ cat escapes.js
let str = 'Hello,\x20World!'
▶ jsluice query -q '(string) @match' escapes.js
"Hello, World!"

If you want the raw data instead of the parsed version, you can use the --raw-output flag:

▶ jsluice query -q '(string) @match' escapes.js --raw-output

'Hello,\x20World!'

Because jsluice also understands JavaScript objects, arrays and so on, one of the coolest things you can do with query mode is extract objects from JavaScript and have them converted to valid JSON, ready for further processing and tweaking using tools like jq or gron.

You can take an object like this one:

const config = {
    stage: false,
    server: "example.com",
    ttl: 3600,
    dns: ["1.1.1.1", "8.8.8.8"],
    paths: {
        "home": "/",
        "blog": "/blog"
    }
}

Turn it into JSON, and then extract just the bits you want using jq:

▶ jsluice query -q '(object) @match' object.js | jq -r 'try .dns[]'
1.1.1.1
8.8.8.8

There's a bunch more you can do with query mode, but that is, as they say, an exercise left for the reader!

Packages

The jsluice command-line tool can do quite a lot, but if you want to integrate jsluice's capabilities into your own code, and even extend those capabilities, you might be pleased to hear that the command-line tool is built on top of the jsluice Go package. This blog post focused almost entirely on the command-line tool as it's the way most people are likely to use jsluice, but as a parting gift, here's a tiny example program using the jsluice package.

package main

import (
    "encoding/json"
    "fmt"
    "github.com/bishopfox/jsluice"
)
 
func main() {
    analyzer := jsluice.NewAnalyzer([]byte(`
        document.location = "/login?redirect=" + url
    `))
 
    for _, url := range analyzer.GetURLs() {
        j, err := json.MarshalIndent(url, "", "  ")
        if err != nil {
            continue
        }
        fmt.Printf("%s\n", j)
    }
}

Thanks for reading this far and let us know what you do with jsluice. To get started, head over to the GitHub Repository. Happy mining!

Subscribe to Bishop Fox's Security Blog

Be first to learn about latest tools, advisories, and findings.


Tom Hudson BF Headshot

About the author, Tom Hudson

Senior Security Analyst

Tom Hudson is a Senior Security Engineer at Bishop Fox, where he is part of the capability development team for Cosmos. He specializes in developing innovative tools that improve the quality of intelligence generated and processed through continuous penetration testing. Tom is the well-known author of numerous command-line tools, which can usually be leveraged together for security research, penetration testing, and bug bounty hunting. His contributions include open source projects such as gron, meg, and unfurl.

Tom is an active member of the information and cybersecurity community and has been a speaker at multiple events including the RSA Conference, BSides Leeds, Agile Yorkshire, the Sky Betting & Gaming Tech Talks, and Hey! Presents. He has also made guest appearances in popular podcasts and YouTube channels, such as HackerOne, Security Weekly, Undetected, STÖK, Web Development Tutorials, and his work has been featured in the Code Maven and Intigriti blogs. He was awarded a Most Valuable Hacker (MVH) belt at the h1-4420 live event in 2019.

Tom enjoys giving back to the community through mentoring and teaching. He has hosted multiple workshops, including a series of talks on cybercrime for UK police and investigators.

More by Tom

This site uses cookies to provide you with a great user experience. By continuing to use our website, you consent to the use of cookies. To find out more about the cookies we use, please see our Privacy Policy.