Skip to content
bitzorcas
中EN

Guide

Website contact forms, leads, and sensitive data

Public CAPTCHA, visitor binding, field limits, Data Protection, notification failure, attribution, lead state, and missing management surface.

Last updated

The contact form is a real public write path into Lead. It has dedicated rate limits, a 32 KiB body limit, one-time CAPTCHA, and protected fields. It lacks an idempotency key, transactional notification, and lead-management API.

1. Call sequence

"Notification publisher""Lead store""CAPTCHA provider""Public endpoint""Browser""Notification publisher""Lead store""CAPTCHA provider""Public endpoint""Browser"GET captchaGenerate(visitor-bound)POST contactVerify onceSave protected LeadPublish lead ID + source

2. Visitor and CAPTCHA

Generation uses purpose public-website, server VisitorId, and remote IP. Submission requires a captcha:public-website:{VisitorId}: challenge prefix and then calls the default provider.

The prefix prevents cross-purpose/visitor use. One-time use, expiry, comparison, and replay resistance belong to the RiskControl provider. The visitor cookie itself is replaceable and is not user identity.

3. Field validation

Name, Contact, InquiryContent, and Source are required. Limits: Name 120, Contact 200, Inquiry 4,000; UTM source/medium 120, campaign 160; challenge 512 and answer 128.

Allowed sources are website, mini-program, referral, and ad. The endpoint accepts a string; Lead.Create rejects unknown normalized values.

4. Sensitive-field protection

Name, Contact, and InquiryContent are protected with ASP.NET Core Data Protection purpose BitzOrcas.Website.Lead.Pii.v1. Notification carries only leadId and source.

Security depends on production key-ring persistence, at-rest protection, sharing, rotation, backup, and disaster recovery. Losing the key ring makes historic lead data unreadable.

ILeadSensitiveDataProtector exposes Protect only, further evidence that a management read surface is not delivered.

5. Attribution

Redis Journey provides first/current touches and a 30-minute session. Lead prefers the journey first touch and stores term/content plus a two-touch JSON snapshot.

Journey failure is logged and contact may continue without attribution. UTM uses heuristic PII rejection, not full DLP.

6. Score semantics

Score starts at 30: campaign +15; inquiry at least 100 characters +25, otherwise +10; referral +20; then clamp 0..100.

This is a deterministic heuristic, not machine learning. Rule version, explanation, fairness review, and outcome calibration are not persisted, so scores across rule changes are not comparable.

7. Save-notify gap

The handler saves first and publishes notification afterward. If save succeeds and notification throws, the request can fail. Retry creates another Lead because there is no idempotency key or business uniqueness.

Current save-then-notify order
// ① After this save, a later failure does not automatically remove the Lead.
Result saved = await repository.SaveAsync(lead, cancellationToken);
if (saved.IsFailure) return Result.Failure<SubmitContactResult>(saved.Error);
// ② Only ID/source leave the module, but an exception can fail HTTP.
await notifications.PublishAsync(new NotificationMessage(
"website.lead.created",
new Dictionary<string, string> { ["leadId"] = lead.Id, ["source"] = lead.Source }),
cancellationToken);
// ③ No idempotency record prevents a duplicate on retry.
return Result.Success(new SubmitContactResult(lead.Id));

GA should use an outbox LeadCreated fact or a durable notification state, plus an idempotency token and response replay window.

8. Lead state machine

Domain supports New→Contacted→Qualified→Converted, Disqualify from nonterminal states, AssignedOwnerId, follow-up append, and attribution snapshots.

There is no lead list/detail/decrypt/assign/follow-up/transition handler or endpoint. website.lead.read/manage is catalog intent, not a CRM capability.

9. Global data model

Lead has no TenantId. Public forms write a platform-global table. If each commercial tenant owns its site/leads, add trusted tenant/owner predicates to model and queries; PublicTenantId in analytics does not fix Lead isolation.

Global mode must state which operators can read all leads and how the key ring is isolated across deployments.

10. Abuse and rendering

CAPTCHA and rate limits reduce automation, but InquiryContent is untrusted. Management UI must context-encode it. Never concatenate it into HTML mail, logs, search queries, or AI prompts.

Define spam/link policy, complaint/deletion workflow, minimal IP-risk signals, and incident runbooks. The current form has no attachments.

11. Test matrix

Cover visitor prefix, bad/expired/replayed CAPTCHA, every size boundary, unknown Source, no plaintext at rest, wrong key ring, unavailable Journey, UTM PII, scoring, save failure, notify-after-save retry, sensitive notification/log absence, distributed limits, body limit, and global/tenant decision.

Notification has minimal fields
// ① Capture the message without decrypting lead fields.
await handler.Handle(validCommand, cancellationToken);
await notifications.Received(1).PublishAsync(
Arg.Is<NotificationMessage>(message =>
message.Parameters.Keys.Order().SequenceEqual(new[] { "leadId", "source" })),
cancellationToken);
// ② Also assert values contain no name, contact, or inquiry.

12. Operations and source check

Measure CAPTCHA rejection, validation rejection, saved leads, notification failure/backlog, duplicate tokens, missing Journey, and protection failures without PII labels.

Terminal window
# Current lead business surface should remain aggregate plus SubmitContact.
rg -n "Lead(Read|Manage)|MarkContacted|Qualify|AddFollowUp|AssignedOwner" \
src/Platform/Website src/Hosts -g '*.cs'

13. Decisions before lead management

  • Is Lead global or tenant-owned?
  • Is idempotency browser-generated or challenge-derived?
  • How long are replay window and successful response retained?
  • Does notification use outbox event or delivery table?
  • Does notification failure still return contact success?
  • Which roles may decrypt which fields?
  • Does decryption require purpose, ticket, or second approval?
  • Is every sensitive read immutably audited?
  • How often is key-ring recovery drilled?
  • Do Lead and Journey share retention?
  • Does deletion clean attribution, follow-ups, and notification copies?
  • How is score-rule version persisted and explained?
  • Do assign/follow-up mutations need optimistic concurrency?
  • How do export/search avoid plaintext indexes?
  • How are spam and false-positive recovery handled?

Back to Website

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%