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
54 changes: 54 additions & 0 deletions tools/v1/team/email-ownership-tracker/docs/SAFETY_LIMITS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Email Ownership Tracker Safety Limits

## Threat Assumptions

Email Ownership Tracker will eventually receive shared-inbox events, ownership
updates, attachment metadata, and team member data from the mail product. Until
an integration issue exists, every external value in this tool folder is treated
as untrusted:

- event ids and message ids can contain path traversal, URL fragments, HTML, or
whitespace that should never become storage keys.
- owner, actor, and team member emails can contain malformed addresses or CRLF
header-injection payloads.
- owner names, reasons, and tags can contain control characters, HTML-like text,
or oversized strings.
- attachment metadata can be malformed, negative, or inflated to force expensive
accounting.
- ownership histories can contain many stale events from shared mailbox imports.

## Unsafe Input Handling

`guards/ownership-boundaries.mjs` provides pure, synchronous helpers that future
services or hooks can call before rendering, storage, notifications, or network
work. The guards:

- reject unsafe ids before they can be used as keys or paths.
- normalize emails to lowercase and reject CRLF or null-byte payloads.
- allow only known ownership actions: `claim`, `assign`, `transfer`, `release`,
and `escalate`.
- sanitize primitive display text by removing control characters and HTML-like
tags, then truncating to field budgets.
- validate attachment metadata only; this tool must not inspect attachment file
bodies in the isolated folder.
- require pagination for oversized histories and team member lists.

The guard layer intentionally does not HTML-escape for a specific renderer. Any
future UI must still escape output according to the rendering framework.

## Performance Limits

Default budgets are deliberately small enough for a V1 launch tool:

| Budget | Default | Reason |
| ----------------- | ------: | ------------------------------------------------ |
| Ownership history | 500 | Bounds one-pass current-owner derivation. |
| Team members | 250 | Prevents scanning large directories in one view. |
| Attachments | 30 | Keeps metadata checks cheap. |
| Attachment bytes | 50 MB | Caps aggregate accounting for one event. |
| Reason text | 1,200 | Avoids rendering imported email threads. |
| Tags | 20 | Prevents unbounded filters and chips. |

Future integration should run these guards before any inbox read, ownership
write, notification fanout, background job, or analytics update. Larger
histories should be paginated and summarized outside the request path.
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
{
"validEvents": [
{
"eventId": "evt-001",
"messageId": "msg-2026-001@example.test",
"action": "claim",
"actorEmail": "lead@example.test",
"ownerEmail": "owner@example.test",
"ownerDisplayName": "Case Owner",
"createdAt": "2026-07-01T09:00:00.000Z",
"reason": "Initial ownership claim for a shared inbox message.",
"teamId": "support-team-1",
"tags": ["vip", "billing", "vip"],
"attachments": [
{
"name": "contract.pdf",
"contentType": "application/pdf",
"sizeBytes": 1024
}
]
},
{
"eventId": "evt-002",
"messageId": "msg-2026-001@example.test",
"action": "release",
"actorEmail": "owner@example.test",
"createdAt": "2026-07-01T10:00:00.000Z",
"reason": "Released after triage."
}
],
"hostileFields": [
{
"id": "path-like-event-id",
"field": "eventId",
"value": "../../etc/passwd",
"reason": "reject path traversal in event identifiers"
},
{
"id": "angle-message-id",
"field": "messageId",
"value": "<script>alert(1)</script>",
"reason": "reject HTML-like message identifiers"
},
{
"id": "unsupported-action",
"field": "action",
"value": "delete-everything",
"reason": "reject actions outside the ownership allowlist"
},
{
"id": "header-injection-actor",
"field": "actorEmail",
"value": "actor@example.test\r\nBcc: victim@example.test",
"reason": "reject CRLF header injection in actor email"
},
{
"id": "missing-owner",
"field": "ownerEmail",
"value": "",
"reason": "require an owner email for ownership-setting actions"
},
{
"id": "bad-created-at",
"field": "createdAt",
"value": "not-a-date",
"reason": "reject timestamps that cannot be parsed"
},
{
"id": "unsafe-tag-shape",
"field": "tags",
"value": ["finance", "../private"],
"reason": "reject unsafe tag characters"
},
{
"id": "bad-attachment-size",
"field": "attachments",
"value": [
{
"name": "invoice.pdf",
"contentType": "application/pdf",
"sizeBytes": -10
}
],
"reason": "reject negative attachment sizes"
}
],
"textCases": [
{
"id": "html-display-name",
"input": "<b>Alice</b>\u0000 Smith",
"expected": "Alice Smith"
},
{
"id": "long-reason",
"inputLength": 1400,
"expectedLength": 1200
}
],
"teamMembers": [
{
"id": "member-1",
"email": "Lead@Example.Test",
"displayName": "Lead Reviewer"
},
{
"id": "member-2",
"email": "backup@example.test",
"displayName": "<i>Backup</i> Reviewer"
}
]
}
Loading