Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions tools/v2/individual/deadline-detector/SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Deadline Detector - Threat Model & Performance Constraints

## Threat Assumptions

1. **Unsafe/Hostile Inputs**: Emails containing malicious scripts or prompt injection payload strings.
2. **Denial of Service (ReDoS)**: Specially crafted strings designed to cause exponential backtracking during regex execution.
3. **Payload Inflation**: Abusively large email bodies (>1MB) attempting to consume memory or freeze UI threads.

## Security Controls

- **Input Sanitization**: All incoming email text is sanitized and stripped of script tags using `sanitizeEmailInput()`.
- **Length Caps**: Input text is hard-capped at 50,000 characters per email.
- **Batching Safeguards**: Maximum batch processing size is restricted to 50 emails per invocation.
- **Regex Bounds**: Regex execution limits iteration caps and utilizes bounded, simple matcher patterns.

## Performance Profile

- **Temporal Keyword Pre-filtering**: Bypasses regex parsing completely if no deadline keywords (`due`, `deadline`, `eod`, etc.) are present.
27 changes: 27 additions & 0 deletions tools/v2/individual/deadline-detector/__tests__/hardening.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { detectDeadlines } from "../services/detector";
import { sanitizeEmailInput, MAX_EMAIL_BODY_LENGTH } from "../security/guards";

describe("Deadline Detector Hardening (#482)", () => {
it("truncates oversized input exceeding maximum length", () => {
const hugeInput = "a".repeat(MAX_EMAIL_BODY_LENGTH + 1000);
const sanitized = sanitizeEmailInput(hugeInput);
expect(sanitized.length).toBe(MAX_EMAIL_BODY_LENGTH);
});

it("strips malicious script tags from email body", () => {
const maliciousInput = 'Please complete task by Friday <script>alert("xss")</script>';
const sanitized = sanitizeEmailInput(maliciousInput);
expect(sanitized).not.toContain("<script>");
});

it("skips non-temporal emails quickly without running heavy matchers", () => {
const emails = [
{ id: "1", body: "Just wanted to say hello and see how you are doing." },
{ id: "2", body: "Project report is due on Friday afternoon." },
];

const results = detectDeadlines(emails);
expect(results.length).toBeGreaterThan(0);
expect(results[0].emailId).toBe("2");
});
});
31 changes: 31 additions & 0 deletions tools/v2/individual/deadline-detector/security/guards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Security & Sanitization Guards for Deadline Detector
*/

export const MAX_EMAIL_BODY_LENGTH = 50_000; // 50 KB max
export const MAX_BATCH_SIZE = 50;

/**
* Sanitizes and truncates input text to protect against payload inflation & script injection.
*/
export function sanitizeEmailInput(text: unknown): string {
if (typeof text !== "string") return "";

// 1. Enforce length cap
let cleaned = text.slice(0, MAX_EMAIL_BODY_LENGTH);

// 2. Strip potential executable script tags / dangerous HTML controls
cleaned = cleaned
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "")
.replace(/javascript:/gi, "");

return cleaned.trim();
}

/**
* Validates array of email payloads for batch processing limits.
*/
export function validateBatchInput<T>(items: T[]): T[] {
if (!Array.isArray(items)) return [];
return items.slice(0, MAX_BATCH_SIZE);
}
76 changes: 76 additions & 0 deletions tools/v2/individual/deadline-detector/services/detector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { sanitizeEmailInput, validateBatchInput } from "../security/guards";

export interface EmailPayload {
id: string;
subject?: string;
body: string;
receivedAt?: string;
}

export interface DetectedDeadline {
emailId: string;
rawText: string;
dueDate: string | null;
confidence: "high" | "medium" | "low";
}

// Fast pre-filter keywords to avoid unnecessary heavy regex parsing
const TEMPORAL_KEYWORDS = [
"due",
"deadline",
"by",
"until",
"eod",
"eow",
"asap",
"submit",
"deliver",
"complete",
"before",
];

/**
* Performance-hardened deadline detector execution.
*/
export function detectDeadlines(rawEmails: EmailPayload[]): DetectedDeadline[] {
const safeEmails = validateBatchInput(rawEmails);
const results: DetectedDeadline[] = [];

for (const email of safeEmails) {
const cleanBody = sanitizeEmailInput(email.body);
const cleanSubject = sanitizeEmailInput(email.subject || "");
const combinedText = `${cleanSubject} ${cleanBody}`.toLowerCase();

// Performance Optimization: Early exit if no temporal keywords exist
const hasTemporalContext = TEMPORAL_KEYWORDS.some((kw) => combinedText.includes(kw));
if (!hasTemporalContext) continue;

// Execute safe extraction logic on sanitized input
const foundDeadlines = extractDatePatterns(email.id, cleanBody);
results.push(...foundDeadlines);
}

return results;
}

function extractDatePatterns(emailId: string, text: string): DetectedDeadline[] {
// ReDoS-safe simple regex patterns or localized date parser call
const datePattern =
/\b(?:due|by|before)\s+(?:on\s+)?([A-Za-z]+\s+\d{1,2}(?:st|nd|rd|th)?|\d{1,2}\/\d{1,2}(?:\/\d{2,4})?)\b/gi;
const matches: DetectedDeadline[] = [];
let match: RegExpExecArray | null;

while ((match = datePattern.exec(text)) !== null) {
matches.push({
emailId,
rawText: match[0],
dueDate: match[1] || null,
confidence: "medium",
});

// Safety guard against infinite loops in regex matching
if (matches.length >= 10) break;
}

return matches;
}
Loading