fix: Suppression-Check nutzt SHA256-Hash statt Klartext-E-Mail (Finding 1)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,7 @@ vi.mock('../db/tenant', () => ({
|
||||
import { checkSuppression } from './check'
|
||||
|
||||
describe('checkSuppression', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
beforeEach(() => mockClient.query.mockReset())
|
||||
|
||||
it('gibt true zurück wenn E-Mail in Suppression-Liste', async () => {
|
||||
mockClient.query.mockResolvedValueOnce([{ exists: true }])
|
||||
@@ -26,21 +26,41 @@ describe('checkSuppression', () => {
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it('normalisiert E-Mail (lowercase + trim) vor dem Check', async () => {
|
||||
it('sucht per SHA256-Hash, nicht per Klartext-E-Mail', async () => {
|
||||
mockClient.query.mockResolvedValueOnce([])
|
||||
await checkSuppression('tenant1', 'Test@Example.com')
|
||||
const [sql, params] = mockClient.query.mock.calls[0] as [string, string[]]
|
||||
expect(sql).toContain('email_hash')
|
||||
expect(sql).not.toContain('email =')
|
||||
// SHA256-Hex ist 64 Zeichen
|
||||
expect(params[0]).toMatch(/^[a-f0-9]{64}$/)
|
||||
})
|
||||
|
||||
it('normalisiert E-Mail vor dem Hashing (lowercase + trim)', async () => {
|
||||
mockClient.query.mockResolvedValueOnce([])
|
||||
await checkSuppression('tenant1', ' TEST@EXAMPLE.COM ')
|
||||
expect(mockClient.query).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
['test@example.com']
|
||||
)
|
||||
|
||||
mockClient.query.mockResolvedValueOnce([])
|
||||
await checkSuppression('tenant1', 'test@example.com')
|
||||
|
||||
const hash1 = (mockClient.query.mock.calls[0] as [string, string[]])[1][0]
|
||||
const hash2 = (mockClient.query.mock.calls[1] as [string, string[]])[1][0]
|
||||
expect(hash1).toBe(hash2)
|
||||
})
|
||||
|
||||
it('Query ist parametrisiert (kein String-Interpolation)', async () => {
|
||||
mockClient.query.mockResolvedValueOnce([])
|
||||
await checkSuppression('tenant1', 'x@x.com')
|
||||
const call = mockClient.query.mock.calls[0]
|
||||
const call = mockClient.query.mock.calls[0] as [string, string[]]
|
||||
expect(call[0]).toContain('$1')
|
||||
expect(call[1]).toEqual(['x@x.com'])
|
||||
expect(call[1]).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('sendet nie die Klartext-E-Mail an die Datenbank', async () => {
|
||||
mockClient.query.mockResolvedValueOnce([])
|
||||
await checkSuppression('tenant1', 'secret@example.com')
|
||||
const [, params] = mockClient.query.mock.calls[0] as [string, string[]]
|
||||
expect(params[0]).not.toContain('secret@example.com')
|
||||
})
|
||||
|
||||
it('propagiert Fehler bei ungültiger tenantId nach oben', async () => {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { withTenant } from '../db/tenant'
|
||||
import { hashEmail } from '../../lib/crypto'
|
||||
|
||||
export async function checkSuppression(tenantId: string, email: string): Promise<boolean> {
|
||||
const normalized = email.toLowerCase().trim()
|
||||
const hash = hashEmail(email)
|
||||
const rows = await withTenant(tenantId, (client) =>
|
||||
client.query<{ '?column?': number }>(
|
||||
'SELECT 1 FROM suppression_list WHERE email = $1 LIMIT 1',
|
||||
[normalized]
|
||||
'SELECT 1 FROM suppression_list WHERE email_hash = $1 LIMIT 1',
|
||||
[hash]
|
||||
)
|
||||
)
|
||||
return rows.length > 0
|
||||
|
||||
Reference in New Issue
Block a user