瀏覽代碼

imessage: drop SMS/RCS by default, opt-in via IMESSAGE_ALLOW_SMS

SMS sender IDs are spoofable; iMessage is Apple-ID-authenticated and
end-to-end encrypted. The plugin previously treated both identically,
so a forged SMS from the owner's own number would match SELF, bypass
the access gate, and inherit owner-level trust — including permission
approval.

handleInbound now drops anything with service != 'iMessage' unless
IMESSAGE_ALLOW_SMS=true. Default is the safe path; users who want SMS
can opt in after reading the warning in README.
Kenneth Lien 5 月之前
父節點
當前提交
60c3fc36ed
共有 2 個文件被更改,包括 9 次插入2 次删除
  1. 1 0
      external_plugins/imessage/README.md
  2. 8 2
      external_plugins/imessage/server.ts

+ 1 - 0
external_plugins/imessage/README.md

@@ -62,6 +62,7 @@ Handles are phone numbers (`+15551234567`) or Apple ID emails (`them@icloud.com`
 | Variable | Default | Effect |
 | --- | --- | --- |
 | `IMESSAGE_APPEND_SIGNATURE` | `true` | Appends `\nSent by Claude` to outbound messages. Set to `false` to disable. |
+| `IMESSAGE_ALLOW_SMS` | `false` | Accept inbound SMS/RCS in addition to iMessage. **Off by default because SMS sender IDs are spoofable** — a forged SMS from your own number would otherwise bypass access control. Only enable if you understand the risk. |
 | `IMESSAGE_ACCESS_MODE` | — | Set to `static` to disable runtime pairing and read `access.json` only. |
 | `IMESSAGE_STATE_DIR` | `~/.claude/channels/imessage` | Override where `access.json` and pairing state live. |
 

+ 8 - 2
external_plugins/imessage/server.ts

@@ -32,6 +32,10 @@ import { join, basename, sep } from 'path'
 
 const STATIC = process.env.IMESSAGE_ACCESS_MODE === 'static'
 const APPEND_SIGNATURE = process.env.IMESSAGE_APPEND_SIGNATURE !== 'false'
+// SMS sender IDs are spoofable; iMessage is Apple-ID-authenticated. Default
+// drops SMS/RCS so a forged sender can't reach the gate. Opt in only if you
+// understand the risk.
+const ALLOW_SMS = process.env.IMESSAGE_ALLOW_SMS === 'true'
 const SIGNATURE = '\nSent by Claude'
 const CHAT_DB = join(homedir(), 'Library', 'Messages', 'chat.db')
 
@@ -104,6 +108,7 @@ type Row = {
   date: number
   is_from_me: number
   cache_has_attachments: number
+  service: string | null
   handle_id: string | null
   chat_guid: string
   chat_style: number | null
@@ -113,7 +118,7 @@ const qWatermark = db.query<{ max: number | null }, []>('SELECT MAX(ROWID) AS ma
 
 const qPoll = db.query<Row, [number]>(`
   SELECT m.ROWID AS rowid, m.guid, m.text, m.attributedBody, m.date, m.is_from_me,
-         m.cache_has_attachments, h.id AS handle_id, c.guid AS chat_guid, c.style AS chat_style
+         m.cache_has_attachments, m.service, h.id AS handle_id, c.guid AS chat_guid, c.style AS chat_style
   FROM message m
   JOIN chat_message_join cmj ON cmj.message_id = m.ROWID
   JOIN chat c ON c.ROWID = cmj.chat_id
@@ -124,7 +129,7 @@ const qPoll = db.query<Row, [number]>(`
 
 const qHistory = db.query<Row, [string, number]>(`
   SELECT m.ROWID AS rowid, m.guid, m.text, m.attributedBody, m.date, m.is_from_me,
-         m.cache_has_attachments, h.id AS handle_id, c.guid AS chat_guid, c.style AS chat_style
+         m.cache_has_attachments, m.service, h.id AS handle_id, c.guid AS chat_guid, c.style AS chat_style
   FROM message m
   JOIN chat_message_join cmj ON cmj.message_id = m.ROWID
   JOIN chat c ON c.ROWID = cmj.chat_id
@@ -710,6 +715,7 @@ function expandTilde(p: string): string {
 
 function handleInbound(r: Row): void {
   if (!r.chat_guid) return
+  if (!ALLOW_SMS && r.service !== 'iMessage') return
 
   // style 45 = DM, 43 = group. Drop unknowns rather than risk routing a
   // group message through the DM gate and leaking a pairing code.