DEV Community

loading...

Sterilizing bash history

lbonanomi
Internet loudmouth since 1996
Updated on ・2 min read

Shared accounts are an unpleasant fact of life for many Linux sysops engineers. This can be a problem when the at-home feeling of a shared account makes you forget that you're not logged into your own shell and you carelessly make a curl call with an inline password or use a command with a --password switch. Let's leverage an obscure bash environment variable and a little bit of go to redact credentials out of our shared shell history.

We need a tool that recognizes commands that accept passwords or tokens in-line and replaces sensitive fields with placeholder text. For the sake of execution speed we'll try using the go language to produce a compiled binary so our shell isn't bogged down too much.

package main

import (
    "bufio"
    "fmt"
    "os"
    "regexp"
    "strings"
)

func curl_u(cmdline string)(after string) {
    curl := regexp.MustCompile(`:\w+?\S+\b`)
    after = curl.ReplaceAllString(cmdline, ":REDACTED ")
    return
}

func https_creds(cmdline string)(after string) {
    pattern := regexp.MustCompile(`https://(\S+?):\S+?@`)
    after = pattern.ReplaceAllString(cmdline, "https://REDACTED:REDACTED@")
    return
}

func header_creds(cmdline string)(after string) {
    pattern := regexp.MustCompile(`(-H|--header)\s.*?(token|auth.*?)\s\S+?\s`)
    after = pattern.ReplaceAllString(cmdline, "-H AUTH_HEADER_REDACTED ")
    return
}


func main() {
    reader := bufio.NewReader(os.Stdin)

    for {
        text, _ := reader.ReadString('\n')

        if (text == "") {
            break
        }

        newtext := ""

        for _, word := range(strings.Fields(text)[1:]) {    // Remove history line number
            newtext = newtext + " " + word                  //
        }

        // Redact credential patterns
        //

        text = newtext

        text = curl_u(text)
        text = https_creds(text)
        text = header_creds(text)

        fmt.Println(text)
    }

As you can see this command only covers a few use-cases that I found to be common in my own history sessions:

 1021  curl -v -H "Authorization: token 3067a4993bd73e857b72d716055bc137283b3a83" https://api.github.com/user
 1022  curl https://lbonanomi:3067a4993bd73e857b72d716055bc137283b3a83@api.github.com/user
 1023  curl -u :3067a4993bd73e857b72d716055bc137283b3a83 https://api.github.com/user

Dumping history and filtering through the go code above we get this much less juicy result:

 curl -v -H AUTH_HEADER_REDACTED https://api.github.com/user
 curl https://REDACTED:REDACTED@api.github.com/user
 curl -u :REDACTED https://api.github.com/user

Pushing history through this command after every execution is going to be a hassle, so let's tweak our .bashrc to run this after every line of input. The bash variable $PROMPT_COMMAND is evaluated every time a user hits the enter key right before the shell returns a command prompt. While its most frequent use is (probably) to change the prompt variable $PS1 it can do anything we want, including executing a shell function like this:

function sterilize_history() {
    history | $HOME/revisionist > sterile && history -r sterile && rm sterile
}

export PROMPT_COMMAND="sterilize_history"

I'm sure a more experienced go programmer will find multitude faults with the code in this article. A gently-worded issue at my nascent go repository would be very-welcome.

This idea was originally explored with a coreutils bash function.

Discussion (0)