π The Problem
Learnova's face recognition attendance system uses Face API.js (components/FaceRecognizer.js) to validate student identity and record attendance. This is one of the platform's headline features:
"AI-powered face recognition using Face API.js for contactless attendance"
"Reduces manual roll-call time dramatically"
However, Face API.js performs static face detection and landmark matching only. It has zero liveness detection β it cannot distinguish between:
- A live student sitting in front of the camera β
- A photograph of that student displayed on a phone or printed and held up β
This means any student who has a photo of a classmate (e.g., from Instagram, WhatsApp, or the institution's portal) can mark attendance on their behalf, completely defeating the system's integrity guarantee.
Attack Scenario (trivially reproducible)
- Student A is absent.
- Student B opens a photo of Student A on their phone screen.
- Student B holds the phone in front of the Learnova camera during attendance.
- Face API.js detects the face, matches descriptors, marks Student A as present.
- No error is thrown. The institution has no indication of spoofing.
This is not theoretical β it is the default failure mode of any webcam-based face recognition system without liveness checks.
π‘ Proposed Fix
1. Add blink detection as a lightweight liveness challenge in components/FaceRecognizer.js
Blink detection uses Eye Aspect Ratio (EAR) from Face API.js's 68-point landmark model, which is already loaded in the project:
// components/FaceRecognizer.js β add liveness detection
const EAR_THRESHOLD = 0.22; // below this = eye closed
const BLINK_CONSEC_FRAMES = 2;
const REQUIRED_BLINKS = 2;
function getEAR(eye) {
// eye: array of 6 {x, y} landmark points
const A = dist(eye[1], eye[5]);
const B = dist(eye[2], eye[4]);
const C = dist(eye[0], eye[3]);
return (A + B) / (2.0 * C);
}
function dist(p1, p2) {
return Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2);
}
// In the detection loop:
let blinkCount = 0;
let blinkFrameCounter = 0;
let livenessVerified = false;
async function runLivenessCheck(videoEl) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('Liveness check timed out')), 15000);
const interval = setInterval(async () => {
const detection = await faceapi
.detectSingleFace(videoEl, new faceapi.TinyFaceDetectorOptions())
.withFaceLandmarks();
if (!detection) return;
const landmarks = detection.landmarks;
const leftEye = landmarks.getLeftEye();
const rightEye = landmarks.getRightEye();
const ear = (getEAR(leftEye) + getEAR(rightEye)) / 2;
if (ear < EAR_THRESHOLD) {
blinkFrameCounter++;
} else {
if (blinkFrameCounter >= BLINK_CONSEC_FRAMES) {
blinkCount++;
}
blinkFrameCounter = 0;
}
if (blinkCount >= REQUIRED_BLINKS) {
clearInterval(interval);
clearTimeout(timer);
resolve(true);
}
}, 100);
});
}
2. Gate attendance recording behind the liveness check
// In the main attendance flow
async function recordAttendance(videoEl) {
setStatus('Please blink twice to verify you are present...');
try {
await runLivenessCheck(videoEl);
} catch (e) {
setStatus('Liveness check failed. Please try again and ensure your face is clearly visible.');
return;
}
// Only proceed with face matching after liveness is confirmed
const descriptor = await getFaceDescriptor(videoEl);
const match = findBestMatch(descriptor);
if (match.distance < MATCH_THRESHOLD) {
await submitAttendance(match.studentId);
setStatus(`β
Attendance recorded for ${match.label}`);
}
}
3. Add a UI instruction prompt for the liveness challenge
// In FaceRecognizer.js render
{livenessStatus === 'pending' && (
<div className="liveness-prompt">
<span className="animate-pulse">ποΈ</span>
<p>Please blink <strong>twice</strong> naturally to confirm you are present.</p>
<p className="text-sm text-muted">Blinks detected: {blinkCount} / {REQUIRED_BLINKS}</p>
</div>
)}
4. Add server-side logging for failed liveness attempts
// In the attendance API route
if (!livenessVerified) {
console.warn(`[ATTENDANCE] Liveness check failed for session ${sessionId} at ${new Date().toISOString()}`);
// Optionally flag to teacher dashboard for manual review
await flagSuspiciousAttempt(sessionId, studentId);
return res.status(403).json({ error: 'Liveness verification required' });
}
π Files to Modify / Create
| File |
Change |
components/FaceRecognizer.js |
Add getEAR(), runLivenessCheck(), blink counter state |
components/AttendanceValidation.js |
Gate recordAttendance() behind liveness check result |
app/attendance/page.js |
Render blink prompt UI with real-time blink counter |
services/authService.js or API route |
Log and flag failed liveness attempts for teacher review |
docs/ |
Add anti-spoofing documentation for institution admins |
π― Why This Matters
Learnova claims to replace manual roll call for institutions, and targets schools and universities where attendance fraud is an active and well-known problem. If the system can be defeated with a smartphone photo in under 10 seconds, it provides a false sense of security that is arguably worse than manual attendance β because teachers stop verifying what the system is recording.
Blink-based liveness detection adds minimal latency (~1-2 seconds), works entirely client-side with Face API.js's existing landmark model, and does not require any new dependencies.
Suggested labels: security, feature, face-recognition, attendance, anti-spoofing
I would like to work on this. Could you please assign it to me?
π The Problem
Learnova's face recognition attendance system uses Face API.js (
components/FaceRecognizer.js) to validate student identity and record attendance. This is one of the platform's headline features:However, Face API.js performs static face detection and landmark matching only. It has zero liveness detection β it cannot distinguish between:
This means any student who has a photo of a classmate (e.g., from Instagram, WhatsApp, or the institution's portal) can mark attendance on their behalf, completely defeating the system's integrity guarantee.
Attack Scenario (trivially reproducible)
This is not theoretical β it is the default failure mode of any webcam-based face recognition system without liveness checks.
π‘ Proposed Fix
1. Add blink detection as a lightweight liveness challenge in
components/FaceRecognizer.jsBlink detection uses Eye Aspect Ratio (EAR) from Face API.js's 68-point landmark model, which is already loaded in the project:
2. Gate attendance recording behind the liveness check
3. Add a UI instruction prompt for the liveness challenge
4. Add server-side logging for failed liveness attempts
π Files to Modify / Create
components/FaceRecognizer.jsgetEAR(),runLivenessCheck(), blink counter statecomponents/AttendanceValidation.jsrecordAttendance()behind liveness check resultapp/attendance/page.jsservices/authService.jsor API routedocs/π― Why This Matters
Learnova claims to replace manual roll call for institutions, and targets schools and universities where attendance fraud is an active and well-known problem. If the system can be defeated with a smartphone photo in under 10 seconds, it provides a false sense of security that is arguably worse than manual attendance β because teachers stop verifying what the system is recording.
Blink-based liveness detection adds minimal latency (~1-2 seconds), works entirely client-side with Face API.js's existing landmark model, and does not require any new dependencies.
Suggested labels:
security,feature,face-recognition,attendance,anti-spoofingI would like to work on this. Could you please assign it to me?