forked from molecule-ai/molecule-core
Merge pull request #709 from Molecule-AI/test/issue-685-686-687-688-regression
test(security): regression suite for input validation fixes (#685 #686 #687 #688)
This commit is contained in:
commit
043d3f83d7
@ -0,0 +1,477 @@
|
||||
package handlers
|
||||
|
||||
// security_regression_685_686_687_688_test.go — regression suite for the
|
||||
// input-validation security fixes shipped in PR #701.
|
||||
//
|
||||
// #686 — GET /templates and GET /org/templates now require AdminAuth
|
||||
// #687 — UUID validation on workspace :id path params (invalid UUID → 400)
|
||||
// #688 — Field length limits: name≤255, role≤1000, model/runtime≤100
|
||||
// #685 — YAML injection: newline/CR characters rejected in name/role/model/runtime
|
||||
//
|
||||
// These tests are intentionally kept at the handler layer (not full router)
|
||||
// for fast CI execution. The template auth tests are the exception — they wire
|
||||
// AdminAuth middleware into a mini gin router to verify the actual security gate
|
||||
// rather than the handler's internal logic.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
sqlmock "github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/Molecule-AI/molecule-monorepo/platform/internal/middleware"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// authTokenQuery matches the SELECT issued by HasAnyLiveTokenGlobal inside AdminAuth.
|
||||
const authTokenQuery = "SELECT COUNT.*workspace_auth_tokens"
|
||||
|
||||
// newEnrolledAuthDB returns a sqlmock DB pre-loaded so that the next
|
||||
// HasAnyLiveTokenGlobal call reports one enrolled workspace (i.e., auth is enforced).
|
||||
// The returned Sqlmock lets the caller verify expectations afterwards.
|
||||
func newEnrolledAuthDB(t *testing.T) (*sql.DB, sqlmock.Sqlmock) {
|
||||
t.Helper()
|
||||
d, m, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("sqlmock.New: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = d.Close() })
|
||||
m.ExpectQuery(authTokenQuery).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1))
|
||||
return d, m
|
||||
}
|
||||
|
||||
// newFreshInstallAuthDB returns a sqlmock DB where HasAnyLiveTokenGlobal
|
||||
// reports zero enrolled workspaces — the platform is in fail-open bootstrap mode.
|
||||
func newFreshInstallAuthDB(t *testing.T) (*sql.DB, sqlmock.Sqlmock) {
|
||||
t.Helper()
|
||||
d, m, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("sqlmock.New: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = d.Close() })
|
||||
m.ExpectQuery(authTokenQuery).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0))
|
||||
return d, m
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// #686 — AdminAuth gate on GET /templates
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// TestSecurity_GetTemplates_NoAuth_Returns401 verifies that once at least one
|
||||
// workspace is enrolled (tokens exist), GET /templates without a bearer token
|
||||
// is rejected with 401. Previously the route was unauthenticated (#686).
|
||||
func TestSecurity_GetTemplates_NoAuth_Returns401(t *testing.T) {
|
||||
setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
authDB, authMock := newEnrolledAuthDB(t)
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
tmplh := NewTemplatesHandler(tmpDir, nil)
|
||||
|
||||
r := gin.New()
|
||||
r.GET("/templates", middleware.AdminAuth(authDB), tmplh.List)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/templates", nil)
|
||||
// Deliberately omit Authorization header — must be rejected.
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("#686 GET /templates no-auth: want 401, got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
if err := authMock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("unmet auth mock expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSecurity_GetTemplates_FreshInstall_FailsOpen verifies that GET /templates
|
||||
// still succeeds on a fresh install (zero enrolled workspaces → AdminAuth fail-open).
|
||||
// This is the regression check: the auth gate must not break new deployments.
|
||||
func TestSecurity_GetTemplates_FreshInstall_FailsOpen(t *testing.T) {
|
||||
setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
authDB, authMock := newFreshInstallAuthDB(t)
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
tmplh := NewTemplatesHandler(tmpDir, nil)
|
||||
|
||||
r := gin.New()
|
||||
r.GET("/templates", middleware.AdminAuth(authDB), tmplh.List)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/templates", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("#686 GET /templates fresh-install: want 200 (fail-open), got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
if err := authMock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("unmet auth mock expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// #686 — AdminAuth gate on GET /org/templates
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// TestSecurity_GetOrgTemplates_NoAuth_Returns401 verifies that GET /org/templates
|
||||
// requires a bearer token once the platform has enrolled workspaces.
|
||||
// Previously the route was unauthenticated, exposing org structure details (#686).
|
||||
func TestSecurity_GetOrgTemplates_NoAuth_Returns401(t *testing.T) {
|
||||
setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
authDB, authMock := newEnrolledAuthDB(t)
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
wh := NewWorkspaceHandler(newTestBroadcaster(), nil, "http://localhost:8080", tmpDir)
|
||||
orgh := NewOrgHandler(wh, newTestBroadcaster(), nil, nil, tmpDir, tmpDir)
|
||||
|
||||
r := gin.New()
|
||||
r.GET("/org/templates", middleware.AdminAuth(authDB), orgh.ListTemplates)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/org/templates", nil)
|
||||
// No Authorization header — must be rejected.
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("#686 GET /org/templates no-auth: want 401, got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
if err := authMock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("unmet auth mock expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSecurity_GetOrgTemplates_FreshInstall_FailsOpen mirrors the /templates
|
||||
// regression check for /org/templates — fresh installs must still work.
|
||||
func TestSecurity_GetOrgTemplates_FreshInstall_FailsOpen(t *testing.T) {
|
||||
setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
authDB, authMock := newFreshInstallAuthDB(t)
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
wh := NewWorkspaceHandler(newTestBroadcaster(), nil, "http://localhost:8080", tmpDir)
|
||||
orgh := NewOrgHandler(wh, newTestBroadcaster(), nil, nil, tmpDir, tmpDir)
|
||||
|
||||
r := gin.New()
|
||||
r.GET("/org/templates", middleware.AdminAuth(authDB), orgh.ListTemplates)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/org/templates", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("#686 GET /org/templates fresh-install: want 200 (fail-open), got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
if err := authMock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("unmet auth mock expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// #687 — UUID validation on workspace :id path params
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// TestSecurity_Get_URLEncodedTraversal_Returns400 verifies that a URL-encoded
|
||||
// path traversal sequence — the type a browser or curl submits as
|
||||
// /workspaces/..%252f..%252fetc%252fpasswd (double-encoded → decoded to
|
||||
// ..%2f..%2fetc%2fpasswd by the HTTP layer) — is rejected 400 before any DB
|
||||
// query. Previously a non-UUID id caused a Postgres syntax error → 500.
|
||||
func TestSecurity_Get_URLEncodedTraversal_Returns400(t *testing.T) {
|
||||
setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewWorkspaceHandler(newTestBroadcaster(), nil, "http://localhost:8080", t.TempDir())
|
||||
|
||||
// gin decodes %25 → %, so the outer HTTP layer hands the handler this value.
|
||||
traversalID := "..%2f..%2fetc%2fpasswd"
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: traversalID}}
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/workspaces/"+traversalID, nil)
|
||||
|
||||
handler.Get(c)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("#687 URL-encoded traversal Get(%q): want 400, got %d body=%s",
|
||||
traversalID, w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestSecurity_Get_NotUUID_Returns400 checks the simplest non-UUID rejection.
|
||||
func TestSecurity_Get_NotUUID_Returns400(t *testing.T) {
|
||||
setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewWorkspaceHandler(newTestBroadcaster(), nil, "http://localhost:8080", t.TempDir())
|
||||
|
||||
for _, badID := range []string{
|
||||
"not-a-uuid",
|
||||
"ws-123",
|
||||
"123",
|
||||
"../etc/passwd",
|
||||
"<script>alert(1)</script>",
|
||||
} {
|
||||
t.Run(badID, func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: badID}}
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/workspaces/"+badID, nil)
|
||||
handler.Get(c)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("#687 Get(%q): want 400, got %d", badID, w.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSecurity_ValidUUID_PassesUUIDValidation verifies that a well-formed UUID
|
||||
// passes the validateWorkspaceID guard — i.e., the fix doesn't false-positive
|
||||
// on legitimate workspace IDs.
|
||||
func TestSecurity_ValidUUID_PassesUUIDValidation(t *testing.T) {
|
||||
if err := validateWorkspaceID("550e8400-e29b-41d4-a716-446655440000"); err != nil {
|
||||
t.Errorf("regression: valid UUID rejected: %v", err)
|
||||
}
|
||||
if err := validateWorkspaceID("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"); err != nil {
|
||||
t.Errorf("regression: valid UUID rejected: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// #688 — Field length limits on POST /workspaces
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// TestSecurity_Create_NameTooLong_Returns400 verifies a 256-character name is
|
||||
// rejected before any DB interaction. The limit is 255 characters (#688).
|
||||
func TestSecurity_Create_NameTooLong_Returns400(t *testing.T) {
|
||||
setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewWorkspaceHandler(newTestBroadcaster(), nil, "http://localhost:8080", t.TempDir())
|
||||
|
||||
name256 := strings.Repeat("a", 256)
|
||||
body := `{"name":"` + name256 + `"}`
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/workspaces", bytes.NewBufferString(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.Create(c)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("#688 name=256 chars: want 400, got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestSecurity_Create_RoleTooLong_Returns400 verifies a 1001-character role is
|
||||
// rejected. The limit is 1000 characters (#688).
|
||||
func TestSecurity_Create_RoleTooLong_Returns400(t *testing.T) {
|
||||
setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewWorkspaceHandler(newTestBroadcaster(), nil, "http://localhost:8080", t.TempDir())
|
||||
|
||||
role1001 := strings.Repeat("r", 1001)
|
||||
body := `{"name":"valid-name","role":"` + role1001 + `"}`
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/workspaces", bytes.NewBufferString(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.Create(c)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("#688 role=1001 chars: want 400, got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestSecurity_Create_ModelTooLong_Returns400 verifies a 101-character model
|
||||
// is rejected (#688). The limit is 100 characters.
|
||||
func TestSecurity_Create_ModelTooLong_Returns400(t *testing.T) {
|
||||
setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewWorkspaceHandler(newTestBroadcaster(), nil, "http://localhost:8080", t.TempDir())
|
||||
|
||||
model101 := strings.Repeat("m", 101)
|
||||
body := `{"name":"valid-name","model":"` + model101 + `"}`
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/workspaces", bytes.NewBufferString(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.Create(c)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("#688 model=101 chars: want 400, got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// #685 — YAML injection: newline/CR rejection
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// TestSecurity_Create_NameWithNewline_Returns400 verifies that a workspace name
|
||||
// containing a literal newline character is rejected before DB interaction.
|
||||
// Newlines break YAML multi-line quoting even with yamlQuote escaping (#685).
|
||||
func TestSecurity_Create_NameWithNewline_Returns400(t *testing.T) {
|
||||
setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewWorkspaceHandler(newTestBroadcaster(), nil, "http://localhost:8080", t.TempDir())
|
||||
|
||||
// JSON \n is a literal newline in the parsed string value.
|
||||
body := `{"name":"bad\nname"}`
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/workspaces", bytes.NewBufferString(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.Create(c)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("#685 name with \\n: want 400, got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestSecurity_Create_YAMLInjectionViaNewline_Returns400 verifies that a
|
||||
// workspace name crafted to inject YAML fields via a newline is caught by the
|
||||
// newline-rejection gate before reaching the provisioner.
|
||||
//
|
||||
// The attack string "agent\nrole: injected_value" would, if written unquoted
|
||||
// into a YAML config, silently set the role field to "injected_value". The
|
||||
// newline is the injection vector — it is rejected by #685.
|
||||
//
|
||||
// Note: curly-brace injection like "{inject: yaml}" does not contain newlines
|
||||
// and is handled separately by yamlQuote escaping in the provisioner
|
||||
// (defence-in-depth). That value is intentionally allowed through here and
|
||||
// must be tested against the provisioner's yamlQuote output, not this gate.
|
||||
func TestSecurity_Create_YAMLInjectionViaNewline_Returns400(t *testing.T) {
|
||||
setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewWorkspaceHandler(newTestBroadcaster(), nil, "http://localhost:8080", t.TempDir())
|
||||
|
||||
// The injected string breaks out of a YAML scalar via newline.
|
||||
body := "{\"name\":\"agent\\nrole: injected_value\"}"
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/workspaces", bytes.NewBufferString(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.Create(c)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("#685 YAML injection via \\n: want 400, got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestSecurity_Create_RoleWithCR_Returns400 verifies carriage-return rejection
|
||||
// in the role field (#685). CR alone can also break YAML multi-line values.
|
||||
func TestSecurity_Create_RoleWithCR_Returns400(t *testing.T) {
|
||||
setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewWorkspaceHandler(newTestBroadcaster(), nil, "http://localhost:8080", t.TempDir())
|
||||
|
||||
body := "{\"name\":\"ok\",\"role\":\"bad\\rrole\"}"
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/workspaces", bytes.NewBufferString(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.Create(c)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("#685 role with \\r: want 400, got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Regression: validateWorkspaceFields direct unit coverage
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// TestSecurity_ValidateWorkspaceFields_BoundaryValues exercises exact-boundary
|
||||
// values for all four field limits to ensure the fence posts are correct.
|
||||
// These are regression checks: fixing the upper limits must not accidentally
|
||||
// tighten or loosen the constraint by ±1.
|
||||
func TestSecurity_ValidateWorkspaceFields_BoundaryValues(t *testing.T) {
|
||||
cases := []struct {
|
||||
label string
|
||||
name string
|
||||
role string
|
||||
model string
|
||||
runtime string
|
||||
wantErr bool
|
||||
}{
|
||||
// Exact maximum lengths — must PASS.
|
||||
{"name_at_255", strings.Repeat("a", 255), "", "", "", false},
|
||||
{"role_at_1000", "", strings.Repeat("r", 1000), "", "", false},
|
||||
{"model_at_100", "", "", strings.Repeat("m", 100), "", false},
|
||||
{"runtime_at_100", "", "", "", strings.Repeat("x", 100), false},
|
||||
// One over the limit — must FAIL.
|
||||
{"name_at_256", strings.Repeat("a", 256), "", "", "", true},
|
||||
{"role_at_1001", "", strings.Repeat("r", 1001), "", "", true},
|
||||
{"model_at_101", "", "", strings.Repeat("m", 101), "", true},
|
||||
{"runtime_at_101", "", "", "", strings.Repeat("x", 101), true},
|
||||
// Newline/CR in each field — must FAIL.
|
||||
{"name_newline", "a\nb", "", "", "", true},
|
||||
{"role_cr", "", "a\rb", "", "", true},
|
||||
{"model_newline", "", "", "a\nb", "", true},
|
||||
{"runtime_newline", "", "", "", "a\nb", true},
|
||||
// Fully valid — must PASS.
|
||||
{"all_valid", "My Agent", "You are a helpful agent.", "claude-opus-4-7", "langgraph", false},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.label, func(t *testing.T) {
|
||||
err := validateWorkspaceFields(tc.name, tc.role, tc.model, tc.runtime)
|
||||
if tc.wantErr && err == nil {
|
||||
t.Errorf("want error, got nil")
|
||||
}
|
||||
if !tc.wantErr && err != nil {
|
||||
t.Errorf("want nil, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSecurity_ValidateWorkspaceID_ValidUUIDs verifies that real workspace UUIDs
|
||||
// (RFC 4122 v4) are accepted. Regression check: the fix must not reject valid IDs.
|
||||
func TestSecurity_ValidateWorkspaceID_ValidUUIDs(t *testing.T) {
|
||||
valid := []string{
|
||||
"550e8400-e29b-41d4-a716-446655440000", // RFC 4122 example
|
||||
"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
|
||||
"00000000-0000-0000-0000-000000000000",
|
||||
"dddddddd-0001-0000-0000-000000000000", // used in other handler tests
|
||||
}
|
||||
for _, id := range valid {
|
||||
if err := validateWorkspaceID(id); err != nil {
|
||||
t.Errorf("regression: valid UUID %q rejected: %v", id, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSecurity_ValidateWorkspaceID_InvalidIDs checks that non-UUID strings all
|
||||
// return errors from validateWorkspaceID.
|
||||
func TestSecurity_ValidateWorkspaceID_InvalidIDs(t *testing.T) {
|
||||
invalid := []string{
|
||||
"not-a-uuid",
|
||||
"ws-abc",
|
||||
"",
|
||||
"../etc/passwd",
|
||||
"..%2f..%2fetc%2fpasswd",
|
||||
"<script>",
|
||||
"1",
|
||||
"00000000-0000-0000-0000", // too short
|
||||
}
|
||||
for _, id := range invalid {
|
||||
if err := validateWorkspaceID(id); err == nil {
|
||||
t.Errorf("expected error for id %q, got nil", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user