Such-API mit Ranking (Relevanz, Datum, Anhangstreffer), mandantengetrennt,
mit Grundoperatoren (Phrase, Ausschluss).
- client.go: Search nutzt jetzt Manticores query_string-Klausel statt
match — unterstützt Phrasensuche ("...") und Ausschluss (-wort) nativ,
Wert bleibt reiner JSON-String ohne dynamischen Feldnamen.
- fieldWeights (statische Konstanten: subject=10, body=3,
attachment_text=1) über die Manticore-Option field_weights — Ranking
berücksichtigt Anhangstreffer, Result.Score macht es nachvollziehbar.
- Bestehenden SRC-01-Injection-Test an die neue query_string-Struktur
angepasst (gleiche Funktion weiterentwickelt).
Prüfungen (alle real durchgeführt, siehe mail/docs/SRC-03-PRUEFPROTOKOLL.md):
1. TestSearch_TenantIsolation (SRC-01, weiterhin gültig).
2. TestSearch_PhraseAndExclusionOperators: Phrase und Ausschluss liefern
real erwartete Teilmengen.
3. TestSearch_PerformanceWithLargeCorpus: Suche über 1000 reale Dokumente
in 775,8µs (Ziel 500ms) gegen echtes Manticore auf 192.168.1.131.
Zusätzlich TestSearch_RankingReflectsFieldWeightAndIsTraceable für
Akzeptanzkriterium 1.
Kein Umbau: dedup/indexworker/storage/crypto/encstorage unverändert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
73 lines
2.4 KiB
Go
73 lines
2.4 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])
|
|
}
|
|
|
|
// query_string hat keinerlei dynamischen Feldnamen — der Klausel-Wert
|
|
// ist unmittelbar der übergebene String, keine map mit datenabhängigem
|
|
// Schlüssel möglich.
|
|
queryStringClause, ok := must[1].(map[string]any)["query_string"]
|
|
if !ok {
|
|
t.Fatal("erwartete 'query_string'-Klausel fehlt")
|
|
}
|
|
if queryStringClause != maliciousQuery {
|
|
t.Fatalf("suchwert wurde verändert: %v", queryStringClause)
|
|
}
|
|
}
|