Manticore-RT-Index für Mail-Suchdokumente (Betreff, Text, Anhangstext, Metadaten), statisches versioniertes Schema. - fields.go: statische Feld-/Index-Namen-Whitelist, einzige Quelle für Feldnamen im Paket (vermeidet known-issues-archivmail.md #11/#12: Sprintf/Join-basierte SQL-Klauselbildung). - migrations/0001_mail_documents.sql: statisches Schema, per go:embed eingebettet, über /sql?mode=raw angelegt (kein String-Zusammenbau). - client.go: Index/Search über die strukturierte Manticore-HTTP-JSON-API, Tenant-Filter über strukturiertes equals-Feld statt WHERE-Interpolation. Prüfungen (alle real durchgeführt, siehe mail/docs/SRC-01-PRUEFPROTOKOLL.md): 1. TestNoDynamicSQLClauseBuilding: automatisierter Quelltext-Scan bestätigt keine Sprintf/Join-SQL-Klauselbildung. 2. TestSearch_MaliciousInputDoesNotAlterFieldNames: Injection-artige Eingaben verändern nachweislich keine Feldnamen im gesendeten Payload. 3. TestSearch_FindsExpectedDocument: Funktionstest gegen echtes Manticore auf 192.168.1.131 liefert erwartete Treffer. Zusätzlich TestSearch_TenantIsolation für Akzeptanzkriterium 3. Kein Umbau: storage/crypto/encstorage/dedup unverändert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
74 lines
2.5 KiB
Go
74 lines
2.5 KiB
Go
package search
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
// TestSearch_MaliciousInputDoesNotAlterFieldNames ist die geforderte
|
|
// Pflichtprüfung 2: eine Abfrage mit manipulierten Eingabewerten
|
|
// (SQL-/Injection-artige Zeichen in tenantSlug und queryText) darf keine
|
|
// Spalten-/Tabellennamen in der an Manticore gesendeten Anfrage verändern
|
|
// — Werte landen ausschließlich als JSON-String-Werte, niemals als
|
|
// Feld-/Tabellenname.
|
|
func TestSearch_MaliciousInputDoesNotAlterFieldNames(t *testing.T) {
|
|
var captured map[string]any
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if err := json.NewDecoder(r.Body).Decode(&captured); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"hits":{"hits":[]}}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
client := NewClient(srv.URL)
|
|
maliciousTenant := `acme"; DROP TABLE mail_documents; --`
|
|
maliciousQuery := `x' OR '1'='1`
|
|
|
|
if _, err := client.Search(context.Background(), maliciousTenant, maliciousQuery); err != nil {
|
|
t.Fatalf("search: %v", err)
|
|
}
|
|
|
|
query, ok := captured["query"].(map[string]any)
|
|
if !ok {
|
|
t.Fatal("erwartetes 'query'-Objekt fehlt in gesendetem Payload")
|
|
}
|
|
boolQuery, ok := query["bool"].(map[string]any)
|
|
if !ok {
|
|
t.Fatal("erwartetes 'bool'-Objekt fehlt")
|
|
}
|
|
must, ok := boolQuery["must"].([]any)
|
|
if !ok || len(must) != 2 {
|
|
t.Fatal("erwartete 'must'-Liste mit 2 Klauseln fehlt")
|
|
}
|
|
|
|
equalsClause, ok := must[0].(map[string]any)["equals"].(map[string]any)
|
|
if !ok {
|
|
t.Fatal("erwartete 'equals'-Klausel fehlt")
|
|
}
|
|
// Feldname bleibt statisch "tenant_slug" — nur der Wert enthält die
|
|
// böswillige Eingabe, unverändert als String.
|
|
if _, hasStaticField := equalsClause[FieldTenantSlug]; !hasStaticField {
|
|
t.Fatalf("erwartetes statisches Feld %q nicht gefunden, habe: %v", FieldTenantSlug, equalsClause)
|
|
}
|
|
if equalsClause[FieldTenantSlug] != maliciousTenant {
|
|
t.Fatalf("wert wurde verändert: %v", equalsClause[FieldTenantSlug])
|
|
}
|
|
|
|
matchClause, ok := must[1].(map[string]any)["match"].(map[string]any)
|
|
if !ok {
|
|
t.Fatal("erwartete 'match'-Klausel fehlt")
|
|
}
|
|
expectedMatchKey := "subject,body,attachment_text"
|
|
if _, hasStaticKey := matchClause[expectedMatchKey]; !hasStaticKey {
|
|
t.Fatalf("erwarteter statischer match-feld-schlüssel %q nicht gefunden, habe: %v", expectedMatchKey, matchClause)
|
|
}
|
|
if matchClause[expectedMatchKey] != maliciousQuery {
|
|
t.Fatalf("suchwert wurde verändert: %v", matchClause[expectedMatchKey])
|
|
}
|
|
}
|