add CSRF protection

This commit is contained in:
Zhi Wang 2022-01-22 18:21:59 -05:00
parent 315759fd52
commit 5052b491f6
11 changed files with 102 additions and 63 deletions

View File

@ -43,7 +43,7 @@ Most icons were provided by [fontawesome](https://fontawesome.com/) under this [
## Installation ## Installation
1. Install the [go](https://go.dev/) compiler. __Make sure your have go 1.17 or higher.__ 1. Install the [go](https://go.dev/) compiler. __Make sure you have go 1.17 or higher.__
2. Download the release and unzip it, or clone the repo 2. Download the release and unzip it, or clone the repo
```git clone https://github.com/syssecfsu/witty.git``` ```git clone https://github.com/syssecfsu/witty.git```
@ -76,11 +76,15 @@ Most icons were provided by [fontawesome](https://fontawesome.com/) under this [
```./witty run -naked htop``` ```./witty run -naked htop```
7. Connect to the server, for example You can also specify the port number WiTTY listens on with ```-p/port```. For example:
```./witty run -p 9090 ssh 192.168.1.2```
7. Connect to the server with your browser, using port 8080 or the one specified in step 6, for example
```https://<witty_server_ip>:8080``` ```https://<witty_server_ip>:8080```
8. You can also replay the recorded sessions with witty 8. You can also replay the recorded sessions with witty. Set your terminal window to 120x36 before using this.
```./witty replay -w 500 records/<recorded file>.scr``` ```./witty replay -w 500 records/<recorded file>.scr```

View File

@ -28,10 +28,11 @@
WiTTY: Web-based interactive TTY WiTTY: Web-based interactive TTY
</a> </a>
<div class="btn-toolbar float-end" role="toolbar" aria-label="top buttons"> <div class="btn-toolbar float-end" role="toolbar" aria-label="top buttons">
<a class="btn btn-primary btn-sm m-1" href="/new" onClick="setTimeout(function(){refresh(true)}, 1000)" <form action="/new" method="post" target="_blank" onsubmit="setTimeout(function(){refresh(true)}, 1000)">
target="_blank" role="button"> {{.csrfField}}
New Session <button class="btn btn-primary btn-sm m-1" type="submit">New Session</button>
</a> </form>
<a class="btn btn-primary btn-sm m-1 {{.disabled}}" href="/logout" role="button"> <a class="btn btn-primary btn-sm m-1 {{.disabled}}" href="/logout" role="button">
Logout Logout
</a> </a>
@ -65,7 +66,13 @@
var active_tab = 0 var active_tab = 0
function del_btn(path) { function del_btn(path) {
fetch("/delete/" + path) let formData = new FormData()
formData.append('gorilla.csrf.Token', {{.csrfToken}})
fetch("/delete/" + path, {
method: "POST",
body: formData,
})
setTimeout(function () { setTimeout(function () {
refresh(true) refresh(true)
}, 20); }, 20);

View File

@ -38,6 +38,9 @@
<label for="passwd">Password</label> <label for="passwd">Password</label>
</div> </div>
<div class="form-floating">
{{.csrfField}}
</div>
<button class="w-100 btn btn-lg btn-primary mt-5" type="submit">Sign in</button> <button class="w-100 btn btn-lg btn-primary mt-5" type="submit">Sign in</button>
<p class="mt-5 mb-3 text-muted">WiTTY: Web-based Interactive TTY</p> <p class="mt-5 mb-3 text-muted">WiTTY: Web-based Interactive TTY</p>
</form> </form>

View File

@ -23,7 +23,8 @@
<nav class="navbar navbar-light bg-light shadow-sm navbar-xs"> <nav class="navbar navbar-light bg-light shadow-sm navbar-xs">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand mx-auto" href="https://github.com/syssecfsu/witty" target="_blank"> <a class="navbar-brand mx-auto" href="https://github.com/syssecfsu/witty" target="_blank">
<img src="/assets/img/{{.logo}}.svg" style="margin-right: 0.5rem;" height="32" class="d-inline-block align-text-top"> <img src="/assets/img/{{.logo}}.svg" style="margin-right: 0.5rem;" height="32"
class="d-inline-block align-text-top">
{{.title}} {{.title}}
</a> </a>
<button type="button" id="record_onoff" class="btn btn-primary btn-sm float-end" value="Record" <button type="button" id="record_onoff" class="btn btn-primary btn-sm float-end" value="Record"
@ -41,15 +42,24 @@
<script> <script>
function recordOnOff(on) { function recordOnOff(on) {
let formData = new FormData()
formData.append('gorilla.csrf.Token', {{.csrfToken}})
var btn = document.getElementById("record_onoff"); var btn = document.getElementById("record_onoff");
if (btn.value == "Record") { if (btn.value == "Record") {
btn.value = "Stop"; btn.value = "Stop";
btn.innerHTML = btn.value btn.innerHTML = btn.value
fetch("/record/{{.id}}") fetch("/record/{{.id}}", {
method: "POST",
body: formData,
})
} else { } else {
btn.value = "Record"; btn.value = "Record";
btn.innerHTML = btn.value btn.innerHTML = btn.value
fetch("/stop/{{.id}}") fetch("/stop/{{.id}}", {
method: "POST",
body: formData,
})
} }
} }

3
go.mod
View File

@ -15,6 +15,7 @@ require (
github.com/gorilla/context v1.1.1 // indirect github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect github.com/gorilla/securecookie v1.1.1 // indirect
github.com/gorilla/sessions v1.2.1 // indirect github.com/gorilla/sessions v1.2.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
) )
require ( require (
@ -24,6 +25,8 @@ require (
github.com/go-playground/universal-translator v0.17.0 // indirect github.com/go-playground/universal-translator v0.17.0 // indirect
github.com/go-playground/validator/v10 v10.4.1 // indirect github.com/go-playground/validator/v10 v10.4.1 // indirect
github.com/golang/protobuf v1.3.3 // indirect github.com/golang/protobuf v1.3.3 // indirect
github.com/gorilla/csrf v1.7.1
github.com/gwatts/gin-adapter v0.0.0-20170508204228-c44433c485ad
github.com/json-iterator/go v1.1.9 // indirect github.com/json-iterator/go v1.1.9 // indirect
github.com/leodido/go-urn v1.2.0 // indirect github.com/leodido/go-urn v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect github.com/mattn/go-isatty v0.0.12 // indirect

6
go.sum
View File

@ -28,6 +28,8 @@ github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/csrf v1.7.1 h1:Ir3o2c1/Uzj6FBxMlAUB6SivgVMy1ONXwYgXn+/aHPE=
github.com/gorilla/csrf v1.7.1/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
@ -35,6 +37,8 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gwatts/gin-adapter v0.0.0-20170508204228-c44433c485ad h1:eGCbPkMnsg02jXBIxxXn1Fxep9dAuTUvEi6UdJsbOhg=
github.com/gwatts/gin-adapter v0.0.0-20170508204228-c44433c485ad/go.mod h1:XywyZk8euPjg6CVt44eMyHjv0sZUiHbHtBnFKgmvj8I=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
@ -45,6 +49,8 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OH
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=

View File

@ -17,9 +17,12 @@ func main() {
} }
var naked bool var naked bool
var port uint
runCmd := flag.NewFlagSet("run", flag.ExitOnError) runCmd := flag.NewFlagSet("run", flag.ExitOnError)
runCmd.BoolVar(&naked, "n", false, "Run WiTTY without user authentication") runCmd.BoolVar(&naked, "n", false, "Run WiTTY without user authentication")
runCmd.BoolVar(&naked, "naked", false, "Run WiTTY without user authentication") runCmd.BoolVar(&naked, "naked", false, "Run WiTTY without user authentication")
runCmd.UintVar(&port, "p", 8080, "Port number to listen on")
runCmd.UintVar(&port, "port", 8080, "Port number to listen on")
var wait uint var wait uint
replayCmd := flag.NewFlagSet("replay", flag.ExitOnError) replayCmd := flag.NewFlagSet("replay", flag.ExitOnError)
@ -65,7 +68,6 @@ func main() {
runCmd.Parse(os.Args[2:]) runCmd.Parse(os.Args[2:])
var cmdToExec []string var cmdToExec []string
args := runCmd.Args() args := runCmd.Args()
if len(args) > 0 { if len(args) > 0 {
cmdToExec = args cmdToExec = args
@ -73,7 +75,7 @@ func main() {
cmdToExec = []string{"bash"} cmdToExec = []string{"bash"}
} }
web.StartWeb(fp, cmdToExec, naked) web.StartWeb(fp, cmdToExec, naked, uint16(port))
default: default:
fmt.Println("witty (adduser|deluser|replay|run)") fmt.Println("witty (adduser|deluser|replay|run)")

View File

@ -36,12 +36,22 @@ const (
stopCmd = 0 stopCmd = 0
) )
// simple function to check origin
func checkOrigin(r *http.Request) bool {
org := r.Header.Get("Origin")
if org != "https://"+r.Host {
log.Println("Failed origin check of ", org, r.Host)
return false
}
return true
}
var upgrader = websocket.Upgrader{ var upgrader = websocket.Upgrader{
ReadBufferSize: readBufferSize, ReadBufferSize: readBufferSize,
WriteBufferSize: WriteBufferSize, WriteBufferSize: WriteBufferSize,
CheckOrigin: func(r *http.Request) bool { CheckOrigin: checkOrigin,
return true
},
} }
// TermConn represents the connected websocket and pty. // TermConn represents the connected websocket and pty.
@ -420,8 +430,7 @@ func ConnectTerm(w http.ResponseWriter, r *http.Request, isViewer bool, name str
} }
} }
func Init(checkOrigin func(r *http.Request) bool) { func Init() {
upgrader.CheckOrigin = checkOrigin
registry.init() registry.init()
} }

View File

@ -6,6 +6,7 @@ import (
"github.com/gin-gonic/contrib/sessions" "github.com/gin-gonic/contrib/sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gorilla/csrf"
) )
const ( const (
@ -48,7 +49,6 @@ func login(c *gin.Context) {
return return
} }
host = &c.Request.Host
c.Redirect(http.StatusSeeOther, "/") c.Redirect(http.StatusSeeOther, "/")
} }
@ -88,5 +88,10 @@ func loginPage(c *gin.Context) {
msg = "Login first" msg = "Login first"
} }
c.HTML(http.StatusOK, "login.html", gin.H{"msg": msg}) c.HTML(http.StatusOK, "login.html",
gin.H{
"msg": msg,
"csrfField": csrf.TemplateField(c.Request),
},
)
} }

View File

@ -5,6 +5,7 @@ import (
"github.com/dchest/uniuri" "github.com/dchest/uniuri"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gorilla/csrf"
"github.com/syssecfsu/witty/term_conn" "github.com/syssecfsu/witty/term_conn"
) )
@ -27,14 +28,18 @@ func collectSessions(c *gin.Context, cmd string) (players []InteractiveSession)
} }
func indexPage(c *gin.Context) { func indexPage(c *gin.Context) {
host = &c.Request.Host
var disabled = "" var disabled = ""
if noAuth { if noAuth {
disabled = "disabled" disabled = "disabled"
} }
c.HTML(http.StatusOK, "index.html", gin.H{"disabled": disabled}) c.HTML(http.StatusOK, "index.html",
gin.H{
"disabled": disabled,
"csrfField": csrf.TemplateField(c.Request),
"csrfToken": csrf.Token(c.Request),
})
} }
func updateIndex(c *gin.Context) { func updateIndex(c *gin.Context) {
@ -63,10 +68,6 @@ func updateIndex(c *gin.Context) {
} }
func newInteractive(c *gin.Context) { func newInteractive(c *gin.Context) {
if host == nil {
host = &c.Request.Host
}
id := uniuri.New() id := uniuri.New()
c.HTML(http.StatusOK, "term.html", gin.H{ c.HTML(http.StatusOK, "term.html", gin.H{
@ -74,6 +75,7 @@ func newInteractive(c *gin.Context) {
"path": "/ws_new/" + id, "path": "/ws_new/" + id,
"id": id, "id": id,
"logo": "keyboard", "logo": "keyboard",
"csrfToken": csrf.Token(c.Request),
}) })
} }
@ -89,6 +91,7 @@ func viewPage(c *gin.Context) {
"path": "/ws_view/" + id, "path": "/ws_view/" + id,
"id": id, "id": id,
"logo": "view", "logo": "view",
"csrfToken": csrf.Token(c.Request),
}) })
} }

View File

@ -1,38 +1,21 @@
package web package web
import ( import (
"log"
"net/http"
"net/url"
"os" "os"
"strconv"
"github.com/dchest/uniuri" "github.com/dchest/uniuri"
"github.com/gin-gonic/contrib/sessions" "github.com/gin-gonic/contrib/sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gorilla/csrf"
adapter "github.com/gwatts/gin-adapter"
"github.com/syssecfsu/witty/term_conn" "github.com/syssecfsu/witty/term_conn"
) )
var host *string = nil
var cmdToExec []string var cmdToExec []string
var noAuth bool var noAuth bool
// simple function to check origin func StartWeb(fp *os.File, cmd []string, naked bool, port uint16) {
func checkOrigin(r *http.Request) bool {
org := r.Header.Get("Origin")
h, err := url.Parse(org)
if err != nil {
return false
}
if (host == nil) || (*host != h.Host) {
log.Println("Failed origin check of ", org)
}
return (host != nil) && (*host == h.Host)
}
func StartWeb(fp *os.File, cmd []string, naked bool) {
cmdToExec = cmd cmdToExec = cmd
noAuth = naked noAuth = naked
@ -47,6 +30,10 @@ func StartWeb(fp *os.File, cmd []string, naked bool) {
store := sessions.NewCookieStore([]byte(uniuri.NewLen(32))) store := sessions.NewCookieStore([]byte(uniuri.NewLen(32)))
rt.Use(sessions.Sessions("witty-session", store)) rt.Use(sessions.Sessions("witty-session", store))
csrfHttp := csrf.Protect([]byte(uniuri.NewLen(32)), csrf.SameSite(csrf.SameSiteStrictMode))
csrfGin := adapter.Wrap(csrfHttp)
rt.Use(csrfGin)
rt.SetTrustedProxies(nil) rt.SetTrustedProxies(nil)
rt.LoadHTMLGlob("./assets/template/*") rt.LoadHTMLGlob("./assets/template/*")
// handle static files // handle static files
@ -71,7 +58,7 @@ func StartWeb(fp *os.File, cmd []string, naked bool) {
g1.GET("/update/:active", updateIndex) g1.GET("/update/:active", updateIndex)
// create a new interactive session // create a new interactive session
g1.GET("/new", newInteractive) g1.POST("/new", newInteractive)
g1.GET("/ws_new/:id", newTermConn) g1.GET("/ws_new/:id", newTermConn)
// create a viewer of an interactive session // create a viewer of an interactive session
@ -79,15 +66,15 @@ func StartWeb(fp *os.File, cmd []string, naked bool) {
g1.GET("/ws_view/:id", newViewWS) g1.GET("/ws_view/:id", newViewWS)
// start/stop recording the session // start/stop recording the session
g1.GET("/record/:id", startRecord) g1.POST("/record/:id", startRecord)
g1.GET("/stop/:id", stopRecord) g1.POST("/stop/:id", stopRecord)
// create a viewer of an interactive session // create a viewer of an interactive session
g1.GET("/replay/:id", replayPage) g1.GET("/replay/:id", replayPage)
// delete a recording // delete a recording
g1.GET("/delete/:fname", delRec) g1.POST("/delete/:fname", delRec)
term_conn.Init(checkOrigin) term_conn.Init()
rt.RunTLS(":8080", "./tls/cert.pem", "./tls/private-key.pem") rt.RunTLS(":"+strconv.FormatUint(uint64(port), 10), "./tls/cert.pem", "./tls/private-key.pem")
} }