Skip to content

proof-of-life passes a single static selfie: burst-free requests report is_real_person true with only face detection #431

Description

@kilodesodiq-arch

Problem

Proof-of-life passes a single static selfie as a "real person" whenever no burst frames are supplied, because the liveness gate defaults to "satisfied" when burst images are absent:

# app/ai-service/proof_of_life.py
burst_required = bool(burst_images_base64)
has_liveness_evidence = (
    checks["blink_detected"] or checks["head_movement_detected"] or not burst_required
)
is_real_person = confidence >= threshold and has_liveness_evidence

With burst_images_base64 empty, burst_required is False, so not burst_required is True, making has_liveness_evidence True regardless of any blink/head-movement signal. liveness_score stays at its 0.40 default, and confidence is computed from face_confidence * 0.50 + quality * 0.20 + liveness * 0.30; a clear, centered face photograph easily clears the default confidence_threshold = 0.65.

Consequence: a request that submits only a selfie — no burst frames — is accepted as a real, live person by the same code path that is meant to detect spoofing. The ProofOfLifeRequest model in main.py makes burst_images_base64 optional, so nothing upstream forces burst frames. The anti-fraud signal that gates claims collapses to "a face is present in one image", which a static photograph or screen recording trivially satisfies.

Root cause

The liveness requirement is conditional on the caller choosing to send burst frames, rather than being a mandatory precondition for a is_real_person = true result.

Why this is architecturally hard

  1. It is a policy/security decision, not a boolean flip. Simply requiring burst_required would break legitimate selfie-only callers; the fix must decide the minimum liveness bar (burst frames? a video? a challenge-response) and reflect that in the request schema, not just the score.
  2. The current liveness detector is heuristic and spoofable. Blink is inferred from eye-count differences between Haar-cascade frames and head movement from bounding-box translation; a printed photo waved or a two-frame replay can satisfy them. Raising the bar means designing a defensible liveness signal, not just adding a field.
  3. It crosses the request contract. ProofOfLifeRequest is a public Pydantic schema consumed by the backend (app/backend/src/verification and the AI client); changing optional→required or adding a liveness_mode is a breaking contract change.
  4. Confidence weighting must stay honest. Any fix must ensure confidence cannot be gamed by a high quality/face score when the liveness evidence is absent; otherwise the threshold remains the weak point.

Proposed design

Make burst/liveness evidence a first-class, required input for a positive result (or add an explicit liveness_mode), and fail closed: if no liveness evidence is supplied, return is_real_person: false with a reason. Optionally add a per-request require_liveness flag so selfie-only callers get an explicit liveness_required refusal rather than a silent pass. Document the minimum acceptable liveness signal.

Acceptance criteria

Service

  • A selfie-only request (no burst frames) returns is_real_person: false with a liveness-related reason.
  • A request with burst frames is scored on actual blink/head-movement evidence, and a static-photo replay does not pass.

Tests

  • A test pins the selfie-only refusal and a test pins a burst-based positive/negative result.

Documentation

  • The AI service README states the minimum liveness requirement so callers (backend/mobile) know to supply burst frames.

Out of scope

Face-matching/1:1 verification against a reference image and fraud-clustering scoring are separate issues.

Getting started

Files: app/ai-service/proof_of_life.py, app/ai-service/main.py (ProofOfLifeRequest/ProofOfLifeResponse).

cd app/ai-service
pytest

Good first files to read: proof_of_life.py analyze/_score_liveness, then main.py ProofOfLifeRequest.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardThird CampaignCampaign: Third Campaignarea:ai-serviceAI service (FastAPI) areabugSomething isn't workinghighHigh severity issues

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions