mirror of
https://github.com/syssecfsu/witty.git
synced 2024-12-25 04:02:36 +01:00
add CSRF protection
This commit is contained in:
parent
315759fd52
commit
5052b491f6
12
README.md
12
README.md
@ -43,7 +43,7 @@ Most icons were provided by [fontawesome](https://fontawesome.com/) under this [
|
||||
|
||||
## 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
|
||||
|
||||
```git clone https://github.com/syssecfsu/witty.git```
|
||||
@ -74,13 +74,17 @@ Most icons were provided by [fontawesome](https://fontawesome.com/) under this [
|
||||
|
||||
If so desired, you can disable user authenticate with ```-n/-naked```, (not recommended) for example:
|
||||
|
||||
```./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```
|
||||
|
||||
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```
|
||||
|
||||
|
@ -28,10 +28,11 @@
|
||||
WiTTY: Web-based interactive TTY
|
||||
</a>
|
||||
<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)"
|
||||
target="_blank" role="button">
|
||||
New Session
|
||||
</a>
|
||||
<form action="/new" method="post" target="_blank" onsubmit="setTimeout(function(){refresh(true)}, 1000)">
|
||||
{{.csrfField}}
|
||||
<button class="btn btn-primary btn-sm m-1" type="submit">New Session</button>
|
||||
</form>
|
||||
|
||||
<a class="btn btn-primary btn-sm m-1 {{.disabled}}" href="/logout" role="button">
|
||||
Logout
|
||||
</a>
|
||||
@ -65,7 +66,13 @@
|
||||
var active_tab = 0
|
||||
|
||||
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 () {
|
||||
refresh(true)
|
||||
}, 20);
|
||||
|
@ -38,6 +38,9 @@
|
||||
<label for="passwd">Password</label>
|
||||
</div>
|
||||
|
||||
<div class="form-floating">
|
||||
{{.csrfField}}
|
||||
</div>
|
||||
<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>
|
||||
</form>
|
||||
|
@ -23,7 +23,8 @@
|
||||
<nav class="navbar navbar-light bg-light shadow-sm navbar-xs">
|
||||
<div class="container-fluid">
|
||||
<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}}
|
||||
</a>
|
||||
<button type="button" id="record_onoff" class="btn btn-primary btn-sm float-end" value="Record"
|
||||
@ -41,15 +42,24 @@
|
||||
|
||||
<script>
|
||||
function recordOnOff(on) {
|
||||
let formData = new FormData()
|
||||
formData.append('gorilla.csrf.Token', {{.csrfToken}})
|
||||
|
||||
var btn = document.getElementById("record_onoff");
|
||||
if (btn.value == "Record") {
|
||||
btn.value = "Stop";
|
||||
btn.innerHTML = btn.value
|
||||
fetch("/record/{{.id}}")
|
||||
fetch("/record/{{.id}}", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
} else {
|
||||
btn.value = "Record";
|
||||
btn.innerHTML = btn.value
|
||||
fetch("/stop/{{.id}}")
|
||||
fetch("/stop/{{.id}}", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,7 +73,7 @@
|
||||
''
|
||||
].join('');
|
||||
|
||||
term.writeln (str)
|
||||
term.writeln(str)
|
||||
}
|
||||
|
||||
Init()
|
||||
|
3
go.mod
3
go.mod
@ -15,6 +15,7 @@ require (
|
||||
github.com/gorilla/context v1.1.1 // indirect
|
||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||
github.com/gorilla/sessions v1.2.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@ -24,6 +25,8 @@ require (
|
||||
github.com/go-playground/universal-translator v0.17.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.4.1 // 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/leodido/go-urn v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.12 // indirect
|
||||
|
6
go.sum
6
go.sum
@ -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/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
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/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
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/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
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/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
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/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/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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
|
6
main.go
6
main.go
@ -17,9 +17,12 @@ func main() {
|
||||
}
|
||||
|
||||
var naked bool
|
||||
var port uint
|
||||
runCmd := flag.NewFlagSet("run", flag.ExitOnError)
|
||||
runCmd.BoolVar(&naked, "n", 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
|
||||
replayCmd := flag.NewFlagSet("replay", flag.ExitOnError)
|
||||
@ -65,7 +68,6 @@ func main() {
|
||||
runCmd.Parse(os.Args[2:])
|
||||
|
||||
var cmdToExec []string
|
||||
|
||||
args := runCmd.Args()
|
||||
if len(args) > 0 {
|
||||
cmdToExec = args
|
||||
@ -73,7 +75,7 @@ func main() {
|
||||
cmdToExec = []string{"bash"}
|
||||
}
|
||||
|
||||
web.StartWeb(fp, cmdToExec, naked)
|
||||
web.StartWeb(fp, cmdToExec, naked, uint16(port))
|
||||
|
||||
default:
|
||||
fmt.Println("witty (adduser|deluser|replay|run)")
|
||||
|
@ -36,12 +36,22 @@ const (
|
||||
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{
|
||||
ReadBufferSize: readBufferSize,
|
||||
WriteBufferSize: WriteBufferSize,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
CheckOrigin: checkOrigin,
|
||||
}
|
||||
|
||||
// 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) {
|
||||
upgrader.CheckOrigin = checkOrigin
|
||||
func Init() {
|
||||
registry.init()
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/csrf"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -48,7 +49,6 @@ func login(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
host = &c.Request.Host
|
||||
c.Redirect(http.StatusSeeOther, "/")
|
||||
}
|
||||
|
||||
@ -88,5 +88,10 @@ func loginPage(c *gin.Context) {
|
||||
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),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/dchest/uniuri"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/csrf"
|
||||
"github.com/syssecfsu/witty/term_conn"
|
||||
)
|
||||
|
||||
@ -27,14 +28,18 @@ func collectSessions(c *gin.Context, cmd string) (players []InteractiveSession)
|
||||
}
|
||||
|
||||
func indexPage(c *gin.Context) {
|
||||
host = &c.Request.Host
|
||||
var disabled = ""
|
||||
|
||||
if noAuth {
|
||||
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) {
|
||||
@ -63,17 +68,14 @@ func updateIndex(c *gin.Context) {
|
||||
}
|
||||
|
||||
func newInteractive(c *gin.Context) {
|
||||
if host == nil {
|
||||
host = &c.Request.Host
|
||||
}
|
||||
|
||||
id := uniuri.New()
|
||||
|
||||
c.HTML(http.StatusOK, "term.html", gin.H{
|
||||
"title": "interactive terminal",
|
||||
"path": "/ws_new/" + id,
|
||||
"id": id,
|
||||
"logo": "keyboard",
|
||||
"title": "interactive terminal",
|
||||
"path": "/ws_new/" + id,
|
||||
"id": id,
|
||||
"logo": "keyboard",
|
||||
"csrfToken": csrf.Token(c.Request),
|
||||
})
|
||||
}
|
||||
|
||||
@ -85,10 +87,11 @@ func newTermConn(c *gin.Context) {
|
||||
func viewPage(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
c.HTML(http.StatusOK, "term.html", gin.H{
|
||||
"title": "viewer terminal",
|
||||
"path": "/ws_view/" + id,
|
||||
"id": id,
|
||||
"logo": "view",
|
||||
"title": "viewer terminal",
|
||||
"path": "/ws_view/" + id,
|
||||
"id": id,
|
||||
"logo": "view",
|
||||
"csrfToken": csrf.Token(c.Request),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1,38 +1,21 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/dchest/uniuri"
|
||||
"github.com/gin-gonic/contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/csrf"
|
||||
adapter "github.com/gwatts/gin-adapter"
|
||||
"github.com/syssecfsu/witty/term_conn"
|
||||
)
|
||||
|
||||
var host *string = nil
|
||||
var cmdToExec []string
|
||||
var noAuth bool
|
||||
|
||||
// simple function to check origin
|
||||
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) {
|
||||
func StartWeb(fp *os.File, cmd []string, naked bool, port uint16) {
|
||||
cmdToExec = cmd
|
||||
noAuth = naked
|
||||
|
||||
@ -47,6 +30,10 @@ func StartWeb(fp *os.File, cmd []string, naked bool) {
|
||||
store := sessions.NewCookieStore([]byte(uniuri.NewLen(32)))
|
||||
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.LoadHTMLGlob("./assets/template/*")
|
||||
// handle static files
|
||||
@ -71,7 +58,7 @@ func StartWeb(fp *os.File, cmd []string, naked bool) {
|
||||
g1.GET("/update/:active", updateIndex)
|
||||
|
||||
// create a new interactive session
|
||||
g1.GET("/new", newInteractive)
|
||||
g1.POST("/new", newInteractive)
|
||||
g1.GET("/ws_new/:id", newTermConn)
|
||||
|
||||
// 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)
|
||||
|
||||
// start/stop recording the session
|
||||
g1.GET("/record/:id", startRecord)
|
||||
g1.GET("/stop/:id", stopRecord)
|
||||
g1.POST("/record/:id", startRecord)
|
||||
g1.POST("/stop/:id", stopRecord)
|
||||
|
||||
// create a viewer of an interactive session
|
||||
g1.GET("/replay/:id", replayPage)
|
||||
|
||||
// delete a recording
|
||||
g1.GET("/delete/:fname", delRec)
|
||||
g1.POST("/delete/:fname", delRec)
|
||||
|
||||
term_conn.Init(checkOrigin)
|
||||
rt.RunTLS(":8080", "./tls/cert.pem", "./tls/private-key.pem")
|
||||
term_conn.Init()
|
||||
rt.RunTLS(":"+strconv.FormatUint(uint64(port), 10), "./tls/cert.pem", "./tls/private-key.pem")
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user