mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-23 23:29:08 +02:00
Submit login credentials URL-encoded and accept them only in that encoding, so AppSec can redact them
This commit is contained in:
@@ -3,6 +3,7 @@ package auth
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"mime"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -25,10 +26,18 @@ func (e *credentialLimitError) Error() string {
|
||||
return "too many authentication attempts"
|
||||
}
|
||||
|
||||
// credentialFormValue returns a PIN or password field from a URL-encoded POST
|
||||
// body. Any other encoding is not a credential submission: AppSec can only
|
||||
// redact URL-encoded bodies, so accepting multipart here would mirror the
|
||||
// credential to the engine in the clear.
|
||||
func credentialFormValue(r *http.Request, field string) string {
|
||||
if r.Method != http.MethodPost {
|
||||
return ""
|
||||
}
|
||||
media, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil || media != "application/x-www-form-urlencoded" {
|
||||
return ""
|
||||
}
|
||||
return r.PostFormValue(field)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/netip"
|
||||
@@ -814,6 +816,69 @@ func TestWasCredentialSubmitted(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCredentialFormValue_OnlyURLEncoded(t *testing.T) {
|
||||
multipartBody := func(t *testing.T) (string, string) {
|
||||
t.Helper()
|
||||
var buf bytes.Buffer
|
||||
w := multipart.NewWriter(&buf)
|
||||
require.NoError(t, w.WriteField(passwordFormId, "secret"))
|
||||
require.NoError(t, w.Close())
|
||||
return buf.String(), w.FormDataContentType()
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
body func(t *testing.T) (string, string)
|
||||
expected string
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "url-encoded",
|
||||
body: func(*testing.T) (string, string) {
|
||||
return url.Values{passwordFormId: {"secret"}}.Encode(), "application/x-www-form-urlencoded"
|
||||
},
|
||||
expected: "secret",
|
||||
description: "a url-encoded body carries the credential",
|
||||
},
|
||||
{
|
||||
// What fetch sends for a URLSearchParams body.
|
||||
name: "url-encoded with charset",
|
||||
body: func(*testing.T) (string, string) {
|
||||
return url.Values{passwordFormId: {"secret"}}.Encode(), "application/x-www-form-urlencoded;charset=UTF-8"
|
||||
},
|
||||
expected: "secret",
|
||||
description: "the charset parameter must not hide the credential",
|
||||
},
|
||||
{
|
||||
// AppSec cannot redact multipart, so a credential in it must not
|
||||
// authenticate, or it would be mirrored to the engine in the clear.
|
||||
name: "multipart",
|
||||
body: multipartBody,
|
||||
expected: "",
|
||||
description: "a multipart body is not a credential submission",
|
||||
},
|
||||
{
|
||||
name: "no content type",
|
||||
body: func(*testing.T) (string, string) {
|
||||
return url.Values{passwordFormId: {"secret"}}.Encode(), ""
|
||||
},
|
||||
expected: "",
|
||||
description: "a body without a content type is not a credential submission",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
body, contentType := tt.body(t)
|
||||
req := httptest.NewRequest(http.MethodPost, "http://example.com/", strings.NewReader(body))
|
||||
if contentType != "" {
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
}
|
||||
assert.Equal(t, tt.expected, credentialFormValue(req, passwordFormId), tt.description)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckIPRestrictions_UnparseableAddress(t *testing.T) {
|
||||
mw := NewMiddleware(log.StandardLogger(), nil, nil)
|
||||
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -52,16 +52,18 @@ function App() {
|
||||
setError(null);
|
||||
setSubmitting(method);
|
||||
|
||||
const formData = new FormData();
|
||||
// URL-encoded, not FormData: the proxy only accepts credentials in this
|
||||
// encoding, the one it can redact before mirroring a request to AppSec.
|
||||
const body = new URLSearchParams();
|
||||
if (method === "password") {
|
||||
formData.append(methods.password!, value);
|
||||
body.append(methods.password!, value);
|
||||
} else {
|
||||
formData.append(methods.pin!, value);
|
||||
body.append(methods.pin!, value);
|
||||
}
|
||||
|
||||
fetch(globalThis.location.href, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
body,
|
||||
redirect: "manual",
|
||||
})
|
||||
.then((res) => {
|
||||
|
||||
Reference in New Issue
Block a user