web admin: add CSRF
This commit is contained in:
parent
f863530653
commit
e9dd4ecdf0
17 changed files with 459 additions and 25 deletions
|
@ -1,6 +1,8 @@
|
|||
package httpd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
|
@ -9,14 +11,16 @@ import (
|
|||
"github.com/rs/xid"
|
||||
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
"github.com/drakkan/sftpgo/utils"
|
||||
)
|
||||
|
||||
type tokenAudience = string
|
||||
|
||||
const (
|
||||
tokenAudienceWeb tokenAudience = "Web"
|
||||
tokenAudienceAPI tokenAudience = "API"
|
||||
tokenAudienceWeb tokenAudience = "Web"
|
||||
tokenAudienceAPI tokenAudience = "API"
|
||||
tokenAudienceCSRF tokenAudience = "CSRF"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -186,3 +190,35 @@ func getAdminFromToken(r *http.Request) *dataprovider.Admin {
|
|||
admin.Permissions = tokenClaims.Permissions
|
||||
return admin
|
||||
}
|
||||
|
||||
func createCSRFToken() string {
|
||||
claims := make(map[string]interface{})
|
||||
now := time.Now().UTC()
|
||||
|
||||
claims[jwt.JwtIDKey] = xid.New().String()
|
||||
claims[jwt.NotBeforeKey] = now.Add(-30 * time.Second)
|
||||
claims[jwt.ExpirationKey] = now.Add(tokenDuration)
|
||||
claims[jwt.AudienceKey] = tokenAudienceCSRF
|
||||
|
||||
_, tokenString, err := csrfTokenAuth.Encode(claims)
|
||||
if err != nil {
|
||||
logger.Debug(logSender, "", "unable to create CSRF token: %v", err)
|
||||
return ""
|
||||
}
|
||||
return tokenString
|
||||
}
|
||||
|
||||
func verifyCSRFToken(tokenString string) error {
|
||||
token, err := jwtauth.VerifyToken(csrfTokenAuth, tokenString)
|
||||
if err != nil || token == nil {
|
||||
logger.Debug(logSender, "", "error validating CSRF: %v", err)
|
||||
return fmt.Errorf("Unable to verify form token: %v", err)
|
||||
}
|
||||
|
||||
if !utils.IsStringInSlice(tokenAudienceCSRF, token.Audience()) {
|
||||
logger.Debug(logSender, "", "error validating CSRF token audience")
|
||||
return errors.New("The form token is not valid")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/jwtauth"
|
||||
|
||||
"github.com/drakkan/sftpgo/common"
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
|
@ -77,6 +78,7 @@ var (
|
|||
jwtTokensCleanupTicker *time.Ticker
|
||||
jwtTokensCleanupDone chan bool
|
||||
invalidatedJWTTokens sync.Map
|
||||
csrfTokenAuth *jwtauth.JWTAuth
|
||||
)
|
||||
|
||||
// Binding defines the configuration for a network listener
|
||||
|
@ -205,6 +207,8 @@ func (c *Conf) Initialize(configDir string) error {
|
|||
certMgr = mgr
|
||||
}
|
||||
|
||||
csrfTokenAuth = jwtauth.New("HS256", utils.GenerateRandomBytes(32), nil)
|
||||
|
||||
exitChannel := make(chan error, 1)
|
||||
|
||||
for _, binding := range c.Bindings {
|
||||
|
|
|
@ -29,6 +29,7 @@ import (
|
|||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/html"
|
||||
|
||||
"github.com/drakkan/sftpgo/common"
|
||||
"github.com/drakkan/sftpgo/config"
|
||||
|
@ -50,6 +51,7 @@ const (
|
|||
defaultTokenAuthPass = "password"
|
||||
altAdminUsername = "newTestAdmin"
|
||||
altAdminPassword = "password1"
|
||||
csrfFormToken = "_form_token"
|
||||
userPath = "/api/v2/users"
|
||||
adminPath = "/api/v2/admins"
|
||||
adminPwdPath = "/api/v2/changepwd/admin"
|
||||
|
@ -3899,8 +3901,10 @@ func TestWebLoginMock(t *testing.T) {
|
|||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusFound, rr)
|
||||
|
||||
csrfToken, err := getCSRFToken()
|
||||
assert.NoError(t, err)
|
||||
// now try using wrong credentials
|
||||
form := getAdminLoginForm(defaultTokenAuthUser, "wrong pwd")
|
||||
form := getAdminLoginForm(defaultTokenAuthUser, "wrong pwd", csrfToken)
|
||||
req, _ = http.NewRequest(http.MethodPost, webLoginPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
rr = executeRequest(req)
|
||||
|
@ -3914,7 +3918,7 @@ func TestWebLoginMock(t *testing.T) {
|
|||
_, _, err = httpdtest.AddAdmin(a, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
||||
form = getAdminLoginForm(altAdminUsername, altAdminPassword)
|
||||
form = getAdminLoginForm(altAdminUsername, altAdminPassword, csrfToken)
|
||||
req, _ = http.NewRequest(http.MethodPost, webLoginPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.RemoteAddr = "127.1.1.1:1234"
|
||||
|
@ -3936,6 +3940,15 @@ func TestWebLoginMock(t *testing.T) {
|
|||
checkResponseCode(t, http.StatusOK, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Login from IP 127.0.1.1:4567 is not allowed")
|
||||
|
||||
// invalid csrf token
|
||||
form = getAdminLoginForm(altAdminUsername, altAdminPassword, "invalid csrf")
|
||||
req, _ = http.NewRequest(http.MethodPost, webLoginPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.RemoteAddr = "10.9.9.8:1234"
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Unable to verify form token")
|
||||
|
||||
req, _ = http.NewRequest(http.MethodGet, webLoginPath, nil)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
|
@ -3973,6 +3986,8 @@ func TestWebAdminPwdChange(t *testing.T) {
|
|||
|
||||
token, err := getJWTWebTokenFromTestServer(admin.Username, altAdminPassword)
|
||||
assert.NoError(t, err)
|
||||
csrfToken, err := getCSRFToken()
|
||||
assert.NoError(t, err)
|
||||
req, _ := http.NewRequest(http.MethodGet, webChangeAdminPwdPath, nil)
|
||||
setJWTCookieForReq(req, token)
|
||||
rr := executeRequest(req)
|
||||
|
@ -3981,6 +3996,15 @@ func TestWebAdminPwdChange(t *testing.T) {
|
|||
form.Set("current_password", altAdminPassword)
|
||||
form.Set("new_password1", altAdminPassword)
|
||||
form.Set("new_password2", altAdminPassword)
|
||||
// no csrf token
|
||||
req, _ = http.NewRequest(http.MethodPost, webChangeAdminPwdPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
setJWTCookieForReq(req, token)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Unable to verify form token")
|
||||
|
||||
form.Set(csrfFormToken, csrfToken)
|
||||
req, _ = http.NewRequest(http.MethodPost, webChangeAdminPwdPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
setJWTCookieForReq(req, token)
|
||||
|
@ -4047,8 +4071,11 @@ func TestBasicWebUsersMock(t *testing.T) {
|
|||
setJWTCookieForReq(req, webToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusNotFound, rr)
|
||||
csrfToken, err := getCSRFToken()
|
||||
assert.NoError(t, err)
|
||||
form := make(url.Values)
|
||||
form.Set("username", user.Username)
|
||||
form.Set(csrfFormToken, csrfToken)
|
||||
b, contentType, _ := getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, webUserPath, &b)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
|
@ -4075,9 +4102,16 @@ func TestBasicWebUsersMock(t *testing.T) {
|
|||
req, _ = http.NewRequest(http.MethodDelete, path.Join(webUserPath, user.Username), nil)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Invalid token")
|
||||
req, _ = http.NewRequest(http.MethodDelete, path.Join(webUserPath, user.Username), nil)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
setCSRFHeaderForReq(req, csrfToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
req, _ = http.NewRequest(http.MethodDelete, path.Join(webUserPath, user1.Username), nil)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
setCSRFHeaderForReq(req, csrfToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
}
|
||||
|
@ -4088,15 +4122,26 @@ func TestWebAdminBasicMock(t *testing.T) {
|
|||
admin := getTestAdmin()
|
||||
admin.Username = altAdminUsername
|
||||
admin.Password = altAdminPassword
|
||||
csrfToken, err := getCSRFToken()
|
||||
assert.NoError(t, err)
|
||||
form := make(url.Values)
|
||||
form.Set("username", admin.Username)
|
||||
form.Set("password", "")
|
||||
form.Set("status", "a") // invalid status
|
||||
form.Set("status", "1")
|
||||
form.Set("permissions", "*")
|
||||
req, _ := http.NewRequest(http.MethodPost, webAdminPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
setJWTCookieForReq(req, token)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Unable to verify form token")
|
||||
|
||||
form.Set(csrfFormToken, csrfToken)
|
||||
form.Set("status", "a")
|
||||
req, _ = http.NewRequest(http.MethodPost, webAdminPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
setJWTCookieForReq(req, token)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
|
||||
form.Set("status", "1")
|
||||
|
@ -4137,6 +4182,15 @@ func TestWebAdminBasicMock(t *testing.T) {
|
|||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusSeeOther, rr)
|
||||
|
||||
form.Set(csrfFormToken, "invalid csrf")
|
||||
req, _ = http.NewRequest(http.MethodPost, path.Join(webAdminPath, altAdminUsername), bytes.NewBuffer([]byte(form.Encode())))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
setJWTCookieForReq(req, token)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Unable to verify form token")
|
||||
|
||||
form.Set(csrfFormToken, csrfToken)
|
||||
form.Set("email", "not-an-email")
|
||||
req, _ = http.NewRequest(http.MethodPost, path.Join(webAdminPath, altAdminUsername), bytes.NewBuffer([]byte(form.Encode())))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
@ -4178,6 +4232,7 @@ func TestWebAdminBasicMock(t *testing.T) {
|
|||
|
||||
req, _ = http.NewRequest(http.MethodDelete, path.Join(webAdminPath, altAdminUsername), nil)
|
||||
setJWTCookieForReq(req, token)
|
||||
setCSRFHeaderForReq(req, csrfToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
|
||||
|
@ -4186,9 +4241,16 @@ func TestWebAdminBasicMock(t *testing.T) {
|
|||
|
||||
req, _ = http.NewRequest(http.MethodDelete, path.Join(webAdminPath, defaultTokenAuthUser), nil)
|
||||
setJWTCookieForReq(req, token)
|
||||
setCSRFHeaderForReq(req, csrfToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
assert.Contains(t, rr.Body.String(), "You cannot delete yourself")
|
||||
|
||||
req, _ = http.NewRequest(http.MethodDelete, path.Join(webAdminPath, defaultTokenAuthUser), nil)
|
||||
setJWTCookieForReq(req, token)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Invalid token")
|
||||
}
|
||||
|
||||
func TestWebAdminPermissions(t *testing.T) {
|
||||
|
@ -4267,12 +4329,15 @@ func TestAdminUpdateSelfMock(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
token, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
assert.NoError(t, err)
|
||||
csrfToken, err := getCSRFToken()
|
||||
assert.NoError(t, err)
|
||||
form := make(url.Values)
|
||||
form.Set("username", admin.Username)
|
||||
form.Set("password", admin.Password)
|
||||
form.Set("status", "0")
|
||||
form.Set("permissions", dataprovider.PermAdminAddUsers)
|
||||
form.Set("permissions", dataprovider.PermAdminCloseConnections)
|
||||
form.Set(csrfFormToken, csrfToken)
|
||||
req, _ := http.NewRequest(http.MethodPost, path.Join(webAdminPath, defaultTokenAuthUser), bytes.NewBuffer([]byte(form.Encode())))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
setJWTCookieForReq(req, token)
|
||||
|
@ -4296,6 +4361,8 @@ func TestWebMaintenanceMock(t *testing.T) {
|
|||
setJWTCookieForReq(req, token)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
csrfToken, err := getCSRFToken()
|
||||
assert.NoError(t, err)
|
||||
|
||||
form := make(url.Values)
|
||||
form.Set("mode", "a")
|
||||
|
@ -4304,6 +4371,15 @@ func TestWebMaintenanceMock(t *testing.T) {
|
|||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Unable to verify form token")
|
||||
|
||||
form.Set(csrfFormToken, csrfToken)
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, webRestorePath, &b)
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
|
||||
form.Set("mode", "0")
|
||||
|
@ -4388,6 +4464,8 @@ func TestWebUserAddMock(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
apiToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
assert.NoError(t, err)
|
||||
csrfToken, err := getCSRFToken()
|
||||
assert.NoError(t, err)
|
||||
user := getTestUser()
|
||||
user.UploadBandwidth = 32
|
||||
user.DownloadBandwidth = 64
|
||||
|
@ -4407,6 +4485,7 @@ func TestWebUserAddMock(t *testing.T) {
|
|||
checkResponseCode(t, http.StatusCreated, rr)
|
||||
|
||||
form := make(url.Values)
|
||||
form.Set(csrfFormToken, csrfToken)
|
||||
form.Set("username", user.Username)
|
||||
form.Set("home_dir", user.HomeDir)
|
||||
form.Set("password", user.Password)
|
||||
|
@ -4528,6 +4607,16 @@ func TestWebUserAddMock(t *testing.T) {
|
|||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
form.Set("max_upload_file_size", "1000")
|
||||
form.Set(csrfFormToken, "invalid form token")
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, webUserPath, &b)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Unable to verify form token")
|
||||
|
||||
form.Set(csrfFormToken, csrfToken)
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, webUserPath, &b)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
|
@ -4622,6 +4711,8 @@ func TestWebUserUpdateMock(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
apiToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
assert.NoError(t, err)
|
||||
csrfToken, err := getCSRFToken()
|
||||
assert.NoError(t, err)
|
||||
user := getTestUser()
|
||||
userAsJSON := getUserAsJSON(t, user)
|
||||
req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))
|
||||
|
@ -4662,7 +4753,17 @@ func TestWebUserUpdateMock(t *testing.T) {
|
|||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Unable to verify form token")
|
||||
|
||||
form.Set(csrfFormToken, csrfToken)
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusSeeOther, rr)
|
||||
|
||||
req, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil)
|
||||
setBearerForReq(req, apiToken)
|
||||
rr = executeRequest(req)
|
||||
|
@ -4755,6 +4856,8 @@ func TestUserTemplateWithFoldersMock(t *testing.T) {
|
|||
|
||||
token, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
assert.NoError(t, err)
|
||||
csrfToken, err := getCSRFToken()
|
||||
assert.NoError(t, err)
|
||||
user := getTestUser()
|
||||
form := make(url.Values)
|
||||
form.Set("username", user.Username)
|
||||
|
@ -4779,6 +4882,15 @@ func TestUserTemplateWithFoldersMock(t *testing.T) {
|
|||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
require.Contains(t, rr.Body.String(), "Unable to verify form token")
|
||||
|
||||
form.Set(csrfFormToken, csrfToken)
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, path.Join(webTemplateUser), &b)
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
require.Contains(t, rr.Body.String(), "invalid folder mapped path")
|
||||
|
||||
|
@ -4828,7 +4940,10 @@ func TestUserTemplateMock(t *testing.T) {
|
|||
user.FsConfig.S3Config.KeyPrefix = "somedir/subdir/"
|
||||
user.FsConfig.S3Config.UploadPartSize = 5
|
||||
user.FsConfig.S3Config.UploadConcurrency = 4
|
||||
csrfToken, err := getCSRFToken()
|
||||
assert.NoError(t, err)
|
||||
form := make(url.Values)
|
||||
form.Set(csrfFormToken, csrfToken)
|
||||
form.Set("username", user.Username)
|
||||
form.Set("home_dir", filepath.Join(os.TempDir(), "%username%"))
|
||||
form.Set("uid", "0")
|
||||
|
@ -4929,6 +5044,8 @@ func TestWebUserS3Mock(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
apiToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
assert.NoError(t, err)
|
||||
csrfToken, err := getCSRFToken()
|
||||
assert.NoError(t, err)
|
||||
user := getTestUser()
|
||||
userAsJSON := getUserAsJSON(t, user)
|
||||
req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))
|
||||
|
@ -4948,6 +5065,7 @@ func TestWebUserS3Mock(t *testing.T) {
|
|||
user.FsConfig.S3Config.UploadPartSize = 5
|
||||
user.FsConfig.S3Config.UploadConcurrency = 4
|
||||
form := make(url.Values)
|
||||
form.Set(csrfFormToken, csrfToken)
|
||||
form.Set("username", user.Username)
|
||||
form.Set("home_dir", user.HomeDir)
|
||||
form.Set("uid", "0")
|
||||
|
@ -5069,6 +5187,8 @@ func TestWebUserGCSMock(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
apiToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
assert.NoError(t, err)
|
||||
csrfToken, err := getCSRFToken()
|
||||
assert.NoError(t, err)
|
||||
user := getTestUser()
|
||||
userAsJSON := getUserAsJSON(t, user)
|
||||
req, err := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))
|
||||
|
@ -5086,6 +5206,7 @@ func TestWebUserGCSMock(t *testing.T) {
|
|||
user.FsConfig.GCSConfig.KeyPrefix = "somedir/subdir/"
|
||||
user.FsConfig.GCSConfig.StorageClass = "standard"
|
||||
form := make(url.Values)
|
||||
form.Set(csrfFormToken, csrfToken)
|
||||
form.Set("username", user.Username)
|
||||
form.Set("home_dir", user.HomeDir)
|
||||
form.Set("uid", "0")
|
||||
|
@ -5167,6 +5288,8 @@ func TestWebUserAzureBlobMock(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
apiToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
assert.NoError(t, err)
|
||||
csrfToken, err := getCSRFToken()
|
||||
assert.NoError(t, err)
|
||||
user := getTestUser()
|
||||
userAsJSON := getUserAsJSON(t, user)
|
||||
req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))
|
||||
|
@ -5185,6 +5308,7 @@ func TestWebUserAzureBlobMock(t *testing.T) {
|
|||
user.FsConfig.AzBlobConfig.UploadConcurrency = 4
|
||||
user.FsConfig.AzBlobConfig.UseEmulator = true
|
||||
form := make(url.Values)
|
||||
form.Set(csrfFormToken, csrfToken)
|
||||
form.Set("username", user.Username)
|
||||
form.Set("home_dir", user.HomeDir)
|
||||
form.Set("uid", "0")
|
||||
|
@ -5286,6 +5410,8 @@ func TestWebUserCryptMock(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
apiToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
assert.NoError(t, err)
|
||||
csrfToken, err := getCSRFToken()
|
||||
assert.NoError(t, err)
|
||||
user := getTestUser()
|
||||
userAsJSON := getUserAsJSON(t, user)
|
||||
req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))
|
||||
|
@ -5297,6 +5423,7 @@ func TestWebUserCryptMock(t *testing.T) {
|
|||
user.FsConfig.Provider = dataprovider.CryptedFilesystemProvider
|
||||
user.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret("crypted passphrase")
|
||||
form := make(url.Values)
|
||||
form.Set(csrfFormToken, csrfToken)
|
||||
form.Set("username", user.Username)
|
||||
form.Set("home_dir", user.HomeDir)
|
||||
form.Set("uid", "0")
|
||||
|
@ -5374,6 +5501,8 @@ func TestWebUserSFTPFsMock(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
apiToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
assert.NoError(t, err)
|
||||
csrfToken, err := getCSRFToken()
|
||||
assert.NoError(t, err)
|
||||
user := getTestUser()
|
||||
userAsJSON := getUserAsJSON(t, user)
|
||||
req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))
|
||||
|
@ -5390,6 +5519,7 @@ func TestWebUserSFTPFsMock(t *testing.T) {
|
|||
user.FsConfig.SFTPConfig.Fingerprints = []string{sftpPkeyFingerprint}
|
||||
user.FsConfig.SFTPConfig.Prefix = "/home/sftpuser"
|
||||
form := make(url.Values)
|
||||
form.Set(csrfFormToken, csrfToken)
|
||||
form.Set("username", user.Username)
|
||||
form.Set("home_dir", user.HomeDir)
|
||||
form.Set("uid", "0")
|
||||
|
@ -5486,6 +5616,8 @@ func TestAddWebFoldersMock(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
apiToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
assert.NoError(t, err)
|
||||
csrfToken, err := getCSRFToken()
|
||||
assert.NoError(t, err)
|
||||
mappedPath := filepath.Clean(os.TempDir())
|
||||
folderName := filepath.Base(mappedPath)
|
||||
form := make(url.Values)
|
||||
|
@ -5496,6 +5628,15 @@ func TestAddWebFoldersMock(t *testing.T) {
|
|||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Unable to verify form token")
|
||||
|
||||
form.Set(csrfFormToken, csrfToken)
|
||||
req, err = http.NewRequest(http.MethodPost, webFolderPath, strings.NewReader(form.Encode()))
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusSeeOther, rr)
|
||||
// adding the same folder will fail since the name must be unique
|
||||
req, err = http.NewRequest(http.MethodPost, webFolderPath, strings.NewReader(form.Encode()))
|
||||
|
@ -5540,6 +5681,8 @@ func TestUpdateWebFolderMock(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
apiToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
assert.NoError(t, err)
|
||||
csrfToken, err := getCSRFToken()
|
||||
assert.NoError(t, err)
|
||||
folderName := "vfolderupdate"
|
||||
folder := vfs.BaseVirtualFolder{
|
||||
Name: folderName,
|
||||
|
@ -5551,11 +5694,21 @@ func TestUpdateWebFolderMock(t *testing.T) {
|
|||
form := make(url.Values)
|
||||
form.Set("mapped_path", newMappedPath)
|
||||
form.Set("name", folderName)
|
||||
form.Set(csrfFormToken, "")
|
||||
req, err := http.NewRequest(http.MethodPost, path.Join(webFolderPath, folderName), strings.NewReader(form.Encode()))
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Unable to verify form token")
|
||||
|
||||
form.Set(csrfFormToken, csrfToken)
|
||||
req, err = http.NewRequest(http.MethodPost, path.Join(webFolderPath, folderName), strings.NewReader(form.Encode()))
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusSeeOther, rr)
|
||||
|
||||
// parse form error
|
||||
|
@ -5595,8 +5748,22 @@ func TestUpdateWebFolderMock(t *testing.T) {
|
|||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusNotFound, rr)
|
||||
|
||||
req, _ = http.NewRequest(http.MethodDelete, path.Join(folderPath, folderName), nil)
|
||||
setBearerForReq(req, apiToken)
|
||||
req, _ = http.NewRequest(http.MethodDelete, path.Join(webFolderPath, folderName), nil)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Invalid token")
|
||||
|
||||
req, _ = http.NewRequest(http.MethodDelete, path.Join(webFolderPath, folderName), nil)
|
||||
setJWTCookieForReq(req, apiToken) // api token is not accepted
|
||||
setCSRFHeaderForReq(req, csrfToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusFound, rr)
|
||||
assert.Equal(t, webLoginPath, rr.Header().Get("Location"))
|
||||
|
||||
req, _ = http.NewRequest(http.MethodDelete, path.Join(webFolderPath, folderName), nil)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
setCSRFHeaderForReq(req, csrfToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
}
|
||||
|
@ -5656,6 +5823,8 @@ func TestWebFoldersMock(t *testing.T) {
|
|||
func TestProviderClosedMock(t *testing.T) {
|
||||
token, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
assert.NoError(t, err)
|
||||
csrfToken, err := getCSRFToken()
|
||||
assert.NoError(t, err)
|
||||
dataprovider.Close()
|
||||
req, _ := http.NewRequest(http.MethodGet, webFoldersPath, nil)
|
||||
setJWTCookieForReq(req, token)
|
||||
|
@ -5670,6 +5839,7 @@ func TestProviderClosedMock(t *testing.T) {
|
|||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusInternalServerError, rr)
|
||||
form := make(url.Values)
|
||||
form.Set(csrfFormToken, csrfToken)
|
||||
form.Set("username", "test")
|
||||
req, _ = http.NewRequest(http.MethodPost, webUserPath+"/0", strings.NewReader(form.Encode()))
|
||||
setJWTCookieForReq(req, token)
|
||||
|
@ -5710,13 +5880,34 @@ func TestProviderClosedMock(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestGetWebConnectionsMock(t *testing.T) {
|
||||
func TestWebConnectionsMock(t *testing.T) {
|
||||
token, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
assert.NoError(t, err)
|
||||
req, _ := http.NewRequest(http.MethodGet, webConnectionsPath, nil)
|
||||
setJWTCookieForReq(req, token)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
|
||||
req, _ = http.NewRequest(http.MethodDelete, path.Join(webConnectionsPath, "id"), nil)
|
||||
setJWTCookieForReq(req, token)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Invalid token")
|
||||
|
||||
req, _ = http.NewRequest(http.MethodDelete, path.Join(webConnectionsPath, "id"), nil)
|
||||
setJWTCookieForReq(req, token)
|
||||
setCSRFHeaderForReq(req, "csrfToken")
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Invalid token")
|
||||
|
||||
csrfToken, err := getCSRFToken()
|
||||
assert.NoError(t, err)
|
||||
req, _ = http.NewRequest(http.MethodDelete, path.Join(webConnectionsPath, "id"), nil)
|
||||
setJWTCookieForReq(req, token)
|
||||
setCSRFHeaderForReq(req, csrfToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusNotFound, rr)
|
||||
}
|
||||
|
||||
func TestGetWebStatusMock(t *testing.T) {
|
||||
|
@ -5776,13 +5967,65 @@ func getUserAsJSON(t *testing.T, user dataprovider.User) []byte {
|
|||
return json
|
||||
}
|
||||
|
||||
func getAdminLoginForm(username, password string) url.Values {
|
||||
func getCSRFToken() (string, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, httpBaseURL+webLoginPath, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
resp, err := httpclient.GetHTTPClient().Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
doc, err := html.Parse(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var csrfToken string
|
||||
var f func(*html.Node)
|
||||
|
||||
f = func(n *html.Node) {
|
||||
if n.Type == html.ElementNode && n.Data == "input" {
|
||||
var name, value string
|
||||
for _, attr := range n.Attr {
|
||||
if attr.Key == "value" {
|
||||
value = attr.Val
|
||||
}
|
||||
if attr.Key == "name" {
|
||||
name = attr.Val
|
||||
}
|
||||
}
|
||||
if name == csrfFormToken {
|
||||
csrfToken = value
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
f(c)
|
||||
}
|
||||
}
|
||||
|
||||
f(doc)
|
||||
|
||||
return csrfToken, nil
|
||||
}
|
||||
|
||||
func getAdminLoginForm(username, password, csrfToken string) url.Values {
|
||||
form := make(url.Values)
|
||||
form.Set("username", username)
|
||||
form.Set("password", password)
|
||||
form.Set(csrfFormToken, csrfToken)
|
||||
return form
|
||||
}
|
||||
|
||||
func setCSRFHeaderForReq(req *http.Request, csrfToken string) {
|
||||
req.Header.Set("X-CSRF-TOKEN", csrfToken)
|
||||
}
|
||||
|
||||
func setBearerForReq(req *http.Request, jwtToken string) {
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", jwtToken))
|
||||
}
|
||||
|
@ -5807,7 +6050,11 @@ func getJWTAPITokenFromTestServer(username, password string) (string, error) {
|
|||
}
|
||||
|
||||
func getJWTWebToken(username, password string) (string, error) {
|
||||
form := getAdminLoginForm(username, password)
|
||||
csrfToken, err := getCSRFToken()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
form := getAdminLoginForm(username, password, csrfToken)
|
||||
req, _ := http.NewRequest(http.MethodPost, httpBaseURL+webLoginPath,
|
||||
bytes.NewBuffer([]byte(form.Encode())))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
@ -5834,7 +6081,11 @@ func getJWTWebToken(username, password string) (string, error) {
|
|||
}
|
||||
|
||||
func getJWTWebTokenFromTestServer(username, password string) (string, error) {
|
||||
form := getAdminLoginForm(username, password)
|
||||
csrfToken, err := getCSRFToken()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
form := getAdminLoginForm(username, password, csrfToken)
|
||||
req, _ := http.NewRequest(http.MethodPost, webLoginPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
rr := executeRequest(req)
|
||||
|
|
|
@ -24,6 +24,7 @@ import (
|
|||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/jwtauth"
|
||||
"github.com/lestrrat-go/jwx/jwt"
|
||||
"github.com/rs/xid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
|
@ -356,6 +357,7 @@ func TestUpdateWebAdminInvalidClaims(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
form := make(url.Values)
|
||||
form.Set(csrfFormToken, createCSRFToken())
|
||||
form.Set("status", "1")
|
||||
req, _ := http.NewRequest(http.MethodPost, path.Join(webAdminPath, "admin"), bytes.NewBuffer([]byte(form.Encode())))
|
||||
rctx := chi.NewRouteContext()
|
||||
|
@ -368,6 +370,49 @@ func TestUpdateWebAdminInvalidClaims(t *testing.T) {
|
|||
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||
}
|
||||
|
||||
func TestCSRFToken(t *testing.T) {
|
||||
// invalid token
|
||||
err := verifyCSRFToken("token")
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "Unable to verify form token")
|
||||
}
|
||||
// bad audience
|
||||
claims := make(map[string]interface{})
|
||||
now := time.Now().UTC()
|
||||
|
||||
claims[jwt.JwtIDKey] = xid.New().String()
|
||||
claims[jwt.NotBeforeKey] = now.Add(-30 * time.Second)
|
||||
claims[jwt.ExpirationKey] = now.Add(tokenDuration)
|
||||
claims[jwt.AudienceKey] = tokenAudienceAPI
|
||||
|
||||
_, tokenString, err := csrfTokenAuth.Encode(claims)
|
||||
assert.NoError(t, err)
|
||||
err = verifyCSRFToken(tokenString)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "form token is not valid")
|
||||
}
|
||||
|
||||
r := GetHTTPRouter()
|
||||
fn := verifyCSRFHeader(r)
|
||||
rr := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodDelete, path.Join(userPath, "username"), nil)
|
||||
fn.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusForbidden, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), "Invalid token")
|
||||
|
||||
req.Header.Set(csrfHeaderToken, tokenString)
|
||||
rr = httptest.NewRecorder()
|
||||
fn.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusForbidden, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), "The token is not valid")
|
||||
|
||||
csrfTokenAuth = jwtauth.New("PS256", utils.GenerateRandomBytes(32), nil)
|
||||
tokenString = createCSRFToken()
|
||||
assert.Empty(t, tokenString)
|
||||
|
||||
csrfTokenAuth = jwtauth.New("HS256", utils.GenerateRandomBytes(32), nil)
|
||||
}
|
||||
|
||||
func TestCreateTokenError(t *testing.T) {
|
||||
server := httpdServer{
|
||||
tokenAuth: jwtauth.New("PS256", utils.GenerateRandomBytes(32), nil),
|
||||
|
@ -386,6 +431,7 @@ func TestCreateTokenError(t *testing.T) {
|
|||
form := make(url.Values)
|
||||
form.Set("username", admin.Username)
|
||||
form.Set("password", admin.Password)
|
||||
form.Set(csrfFormToken, createCSRFToken())
|
||||
req, _ = http.NewRequest(http.MethodPost, webLoginPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
req.RemoteAddr = "127.0.0.1:1234"
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
@ -470,6 +516,7 @@ func TestAdminAllowListConnAddr(t *testing.T) {
|
|||
req.RemoteAddr = "192.168.1.16:1234"
|
||||
server.checkAddrAndSendToken(rr, req.WithContext(ctx), admin)
|
||||
assert.Equal(t, http.StatusForbidden, rr.Code, rr.Body.String())
|
||||
assert.Equal(t, "context value connection address", connAddrKey.String())
|
||||
}
|
||||
|
||||
func TestUpdateContextFromCookie(t *testing.T) {
|
||||
|
|
|
@ -2,6 +2,7 @@ package httpd
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/jwtauth"
|
||||
|
@ -11,9 +12,15 @@ import (
|
|||
"github.com/drakkan/sftpgo/utils"
|
||||
)
|
||||
|
||||
type ctxKeyConnAddr int
|
||||
var connAddrKey = &contextKey{"connection address"}
|
||||
|
||||
const connAddrKey ctxKeyConnAddr = 0
|
||||
type contextKey struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (k *contextKey) String() string {
|
||||
return "context value " + k.name
|
||||
}
|
||||
|
||||
func saveConnectionAddress(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -26,14 +33,14 @@ func jwtAuthenticator(next http.Handler) http.Handler {
|
|||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token, _, err := jwtauth.FromContext(r.Context())
|
||||
|
||||
if err != nil {
|
||||
if err != nil || token == nil {
|
||||
logger.Debug(logSender, "", "error getting jwt token: %v", err)
|
||||
sendAPIResponse(w, r, err, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
err = jwt.Validate(token)
|
||||
if token == nil || err != nil {
|
||||
if err != nil {
|
||||
logger.Debug(logSender, "", "error validating jwt token: %v", err)
|
||||
sendAPIResponse(w, r, err, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
return
|
||||
|
@ -58,14 +65,14 @@ func jwtAuthenticatorWeb(next http.Handler) http.Handler {
|
|||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token, _, err := jwtauth.FromContext(r.Context())
|
||||
|
||||
if err != nil {
|
||||
if err != nil || token == nil {
|
||||
logger.Debug(logSender, "", "error getting web jwt token: %v", err)
|
||||
http.Redirect(w, r, webLoginPath, http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
err = jwt.Validate(token)
|
||||
if token == nil || err != nil {
|
||||
if err != nil {
|
||||
logger.Debug(logSender, "", "error validating web jwt token: %v", err)
|
||||
http.Redirect(w, r, webLoginPath, http.StatusFound)
|
||||
return
|
||||
|
@ -114,3 +121,23 @@ func checkPerm(perm string) func(next http.Handler) http.Handler {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func verifyCSRFHeader(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
tokenString := r.Header.Get(csrfHeaderToken)
|
||||
token, err := jwtauth.VerifyToken(csrfTokenAuth, tokenString)
|
||||
if err != nil || token == nil {
|
||||
logger.Debug(logSender, "", "error validating CSRF header: %v", err)
|
||||
sendAPIResponse(w, r, err, "Invalid token", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if !utils.IsStringInSlice(tokenAudienceCSRF, token.Audience()) {
|
||||
logger.Debug(logSender, "", "error validating CSRF header audience")
|
||||
sendAPIResponse(w, r, errors.New("The token is not valid"), "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -109,6 +109,10 @@ func (s *httpdServer) handleWebLoginPost(w http.ResponseWriter, r *http.Request)
|
|||
renderLoginPage(w, "Invalid credentials")
|
||||
return
|
||||
}
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
|
||||
renderLoginPage(w, err.Error())
|
||||
return
|
||||
}
|
||||
admin, err := dataprovider.CheckAdminAndPass(username, password, utils.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
if err != nil {
|
||||
renderLoginPage(w, err.Error())
|
||||
|
@ -367,16 +371,21 @@ func (s *httpdServer) initializeRouter() {
|
|||
Get(webAdminPath+"/{username}", handleWebUpdateAdminGet)
|
||||
router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Post(webAdminPath, handleWebAddAdminPost)
|
||||
router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Post(webAdminPath+"/{username}", handleWebUpdateAdminPost)
|
||||
router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Delete(webAdminPath+"/{username}", deleteAdmin)
|
||||
router.With(checkPerm(dataprovider.PermAdminCloseConnections)).
|
||||
router.With(checkPerm(dataprovider.PermAdminManageAdmins), verifyCSRFHeader).
|
||||
Delete(webAdminPath+"/{username}", deleteAdmin)
|
||||
router.With(checkPerm(dataprovider.PermAdminCloseConnections), verifyCSRFHeader).
|
||||
Delete(webConnectionsPath+"/{connectionID}", handleCloseConnection)
|
||||
router.With(checkPerm(dataprovider.PermAdminChangeUsers), s.refreshCookie).
|
||||
Get(webFolderPath+"/{name}", handleWebUpdateFolderGet)
|
||||
router.With(checkPerm(dataprovider.PermAdminChangeUsers)).Post(webFolderPath+"/{name}", handleWebUpdateFolderPost)
|
||||
router.With(checkPerm(dataprovider.PermAdminDeleteUsers)).Delete(webFolderPath+"/{name}", deleteFolder)
|
||||
router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Post(webScanVFolderPath, startVFolderQuotaScan)
|
||||
router.With(checkPerm(dataprovider.PermAdminDeleteUsers)).Delete(webUserPath+"/{username}", deleteUser)
|
||||
router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Post(webQuotaScanPath, startQuotaScan)
|
||||
router.With(checkPerm(dataprovider.PermAdminDeleteUsers), verifyCSRFHeader).
|
||||
Delete(webFolderPath+"/{name}", deleteFolder)
|
||||
router.With(checkPerm(dataprovider.PermAdminQuotaScans), verifyCSRFHeader).
|
||||
Post(webScanVFolderPath, startVFolderQuotaScan)
|
||||
router.With(checkPerm(dataprovider.PermAdminDeleteUsers), verifyCSRFHeader).
|
||||
Delete(webUserPath+"/{username}", deleteUser)
|
||||
router.With(checkPerm(dataprovider.PermAdminQuotaScans), verifyCSRFHeader).
|
||||
Post(webQuotaScanPath, startQuotaScan)
|
||||
router.With(checkPerm(dataprovider.PermAdminManageSystem)).Get(webMaintenancePath, handleWebMaintenance)
|
||||
router.With(checkPerm(dataprovider.PermAdminManageSystem)).Get(webBackupPath, dumpData)
|
||||
router.With(checkPerm(dataprovider.PermAdminManageSystem)).Post(webRestorePath, handleWebRestore)
|
||||
|
|
46
httpd/web.go
46
httpd/web.go
|
@ -68,6 +68,8 @@ const (
|
|||
defaultQueryLimit = 500
|
||||
webDateTimeFormat = "2006-01-02 15:04:05" // YYYY-MM-DD HH:MM:SS
|
||||
redactedSecret = "[**redacted**]"
|
||||
csrfFormToken = "_form_token"
|
||||
csrfHeaderToken = "X-CSRF-TOKEN"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -98,6 +100,7 @@ type basePage struct {
|
|||
StatusTitle string
|
||||
MaintenanceTitle string
|
||||
Version string
|
||||
CSRFToken string
|
||||
LoggedAdmin *dataprovider.Admin
|
||||
}
|
||||
|
||||
|
@ -175,6 +178,7 @@ type loginPage struct {
|
|||
CurrentURL string
|
||||
Version string
|
||||
Error string
|
||||
CSRFToken string
|
||||
}
|
||||
|
||||
type userTemplateFields struct {
|
||||
|
@ -259,6 +263,10 @@ func loadTemplates(templatesPath string) {
|
|||
}
|
||||
|
||||
func getBasePageData(title, currentURL string, r *http.Request) basePage {
|
||||
var csrfToken string
|
||||
if currentURL != "" {
|
||||
csrfToken = createCSRFToken()
|
||||
}
|
||||
return basePage{
|
||||
Title: title,
|
||||
CurrentURL: currentURL,
|
||||
|
@ -284,6 +292,7 @@ func getBasePageData(title, currentURL string, r *http.Request) basePage {
|
|||
MaintenanceTitle: pageMaintenanceTitle,
|
||||
Version: version.GetAsString(),
|
||||
LoggedAdmin: getAdminFromToken(r),
|
||||
CSRFToken: csrfToken,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -946,6 +955,7 @@ func renderLoginPage(w http.ResponseWriter, error string) {
|
|||
CurrentURL: webLoginPath,
|
||||
Version: version.Get().Version,
|
||||
Error: error,
|
||||
CSRFToken: createCSRFToken(),
|
||||
}
|
||||
renderTemplate(w, templateLogin, data)
|
||||
}
|
||||
|
@ -961,6 +971,10 @@ func handleWebAdminChangePwdPost(w http.ResponseWriter, r *http.Request) {
|
|||
renderChangePwdPage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
|
||||
renderForbiddenPage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
err = doChangeAdminPassword(r, r.Form.Get("current_password"), r.Form.Get("new_password1"),
|
||||
r.Form.Get("new_password2"))
|
||||
if err != nil {
|
||||
|
@ -991,6 +1005,10 @@ func handleWebRestore(w http.ResponseWriter, r *http.Request) {
|
|||
renderMaintenancePage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
|
||||
renderForbiddenPage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
restoreMode, err := strconv.Atoi(r.Form.Get("mode"))
|
||||
if err != nil {
|
||||
renderMaintenancePage(w, r, err.Error())
|
||||
|
@ -1077,6 +1095,10 @@ func handleWebAddAdminPost(w http.ResponseWriter, r *http.Request) {
|
|||
renderAddUpdateAdminPage(w, r, &admin, err.Error(), true)
|
||||
return
|
||||
}
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
|
||||
renderForbiddenPage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
err = dataprovider.AddAdmin(&admin)
|
||||
if err != nil {
|
||||
renderAddUpdateAdminPage(w, r, &admin, err.Error(), true)
|
||||
|
@ -1103,6 +1125,10 @@ func handleWebUpdateAdminPost(w http.ResponseWriter, r *http.Request) {
|
|||
renderAddUpdateAdminPage(w, r, &updatedAdmin, err.Error(), false)
|
||||
return
|
||||
}
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
|
||||
renderForbiddenPage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
updatedAdmin.ID = admin.ID
|
||||
updatedAdmin.Username = admin.Username
|
||||
if updatedAdmin.Password == "" {
|
||||
|
@ -1184,6 +1210,10 @@ func handleWebTemplateUserPost(w http.ResponseWriter, r *http.Request) {
|
|||
renderMessagePage(w, r, "Error parsing user fields", "", http.StatusBadRequest, err, "")
|
||||
return
|
||||
}
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
|
||||
renderForbiddenPage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var dump dataprovider.BackupData
|
||||
dump.Version = dataprovider.DumpVersion
|
||||
|
@ -1251,6 +1281,10 @@ func handleWebAddUserPost(w http.ResponseWriter, r *http.Request) {
|
|||
renderUserPage(w, r, &user, userPageModeAdd, err.Error())
|
||||
return
|
||||
}
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
|
||||
renderForbiddenPage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
err = dataprovider.AddUser(&user)
|
||||
if err == nil {
|
||||
http.Redirect(w, r, webUsersPath, http.StatusSeeOther)
|
||||
|
@ -1275,6 +1309,10 @@ func handleWebUpdateUserPost(w http.ResponseWriter, r *http.Request) {
|
|||
renderUserPage(w, r, &user, userPageModeUpdate, err.Error())
|
||||
return
|
||||
}
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
|
||||
renderForbiddenPage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
updatedUser.ID = user.ID
|
||||
updatedUser.Username = user.Username
|
||||
updatedUser.SetEmptySecretsIfNil()
|
||||
|
@ -1325,6 +1363,10 @@ func handleWebAddFolderPost(w http.ResponseWriter, r *http.Request) {
|
|||
renderFolderPage(w, r, folder, folderPageModeAdd, err.Error())
|
||||
return
|
||||
}
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
|
||||
renderForbiddenPage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
folder.MappedPath = r.Form.Get("mapped_path")
|
||||
folder.Name = r.Form.Get("name")
|
||||
|
||||
|
@ -1365,6 +1407,10 @@ func handleWebUpdateFolderPost(w http.ResponseWriter, r *http.Request) {
|
|||
renderFolderPage(w, r, folder, folderPageModeUpdate, err.Error())
|
||||
return
|
||||
}
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
|
||||
renderForbiddenPage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
folder.MappedPath = r.Form.Get("mapped_path")
|
||||
err = dataprovider.UpdateFolder(&folder)
|
||||
if err != nil {
|
||||
|
|
|
@ -88,6 +88,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
|
||||
<button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -99,6 +99,7 @@
|
|||
url: path,
|
||||
type: 'DELETE',
|
||||
dataType: 'json',
|
||||
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
||||
timeout: 15000,
|
||||
success: function (result) {
|
||||
table.button('delete:name').enable(true);
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
|
||||
<button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">Change my password</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -98,6 +98,7 @@
|
|||
url: path,
|
||||
type: 'DELETE',
|
||||
dataType: 'json',
|
||||
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
||||
timeout: 15000,
|
||||
success: function (result) {
|
||||
setTimeout(function () {
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
|
||||
<button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -97,6 +97,7 @@ function deleteAction() {
|
|||
url: encodeURI(path),
|
||||
type: 'DELETE',
|
||||
dataType: 'json',
|
||||
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
||||
timeout: 15000,
|
||||
success: function (result) {
|
||||
table.button('delete:name').enable(true);
|
||||
|
@ -160,6 +161,7 @@ function deleteAction() {
|
|||
url: path,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
||||
data: JSON.stringify({ "name": folderName }),
|
||||
timeout: 15000,
|
||||
success: function (result) {
|
||||
|
|
|
@ -80,7 +80,7 @@
|
|||
<input type="password" class="form-control form-control-user-custom"
|
||||
id="inputPassword" name="password" placeholder="Password">
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
|
||||
<button type="submit" class="btn btn-primary btn-user-custom btn-block">
|
||||
Login
|
||||
</button>
|
||||
|
|
|
@ -44,6 +44,7 @@
|
|||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
|
||||
<button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">Import</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -583,7 +583,10 @@
|
|||
<label for="idSFTPEndpoint" class="col-sm-2 col-form-label">Endpoint</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="text" class="form-control" id="idSFTPEndpoint" name="sftp_endpoint" placeholder=""
|
||||
value="{{.User.FsConfig.SFTPConfig.Endpoint}}" maxlength="255">
|
||||
value="{{.User.FsConfig.SFTPConfig.Endpoint}}" maxlength="255" aria-describedby="SFTPEndpointHelpBlock">
|
||||
<small id="SFTPEndpointHelpBlock" class="form-text text-muted">
|
||||
Endpoint as host:port, port is always required
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-sm-2"></div>
|
||||
<label for="idSFTPUsername" class="col-sm-2 col-form-label">Username</label>
|
||||
|
@ -659,6 +662,7 @@
|
|||
{{end}}
|
||||
|
||||
<input type="hidden" name="expiration_date" id="hidden_start_datetime" value="">
|
||||
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
|
||||
<button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">{{if eq .Mode 3}}Generate and export users{{else}}Submit{{end}}</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -105,6 +105,7 @@
|
|||
url: encodeURI(path),
|
||||
type: 'DELETE',
|
||||
dataType: 'json',
|
||||
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
||||
timeout: 15000,
|
||||
success: function (result) {
|
||||
table.button('delete:name').enable(true);
|
||||
|
@ -199,6 +200,7 @@
|
|||
url: path,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
||||
data: JSON.stringify({ "username": username }),
|
||||
timeout: 15000,
|
||||
success: function (result) {
|
||||
|
|
Loading…
Reference in a new issue