safari-mcp 2.22.0 → 2.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/transport.js +28 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "safari-mcp",
3
- "version": "2.22.0",
3
+ "version": "2.22.1",
4
4
  "mcpName": "io.github.achiya-automation/safari-mcp",
5
5
  "description": "Safari browser automation for AI agents \u2014 native macOS, zero Chrome overhead. 98 tools via AppleScript + JavaScript.",
6
6
  "type": "module",
package/transport.js CHANGED
@@ -18,7 +18,7 @@ export function planTransport(env = {}) {
18
18
  if (flag && flag !== "0") {
19
19
  return {
20
20
  kind: "http",
21
- host: "127.0.0.1", // localhost-only bind — no auth needed, never exposed off-box
21
+ host: "127.0.0.1", // loopback-only bind — necessary, not sufficient: see isLoopbackRequest()
22
22
  port: parseInt(env.SAFARI_MCP_HTTP_PORT || String(DEFAULT_HTTP_PORT), 10),
23
23
  };
24
24
  }
@@ -48,6 +48,15 @@ export async function startTransport(createMcpServer, env = process.env) {
48
48
 
49
49
  const httpServer = createServer(async (req, res) => {
50
50
  try {
51
+ const { port } = /** @type {import("node:net").AddressInfo} */ (httpServer.address());
52
+ if (!isLoopbackRequest(req.headers, port)) {
53
+ res.statusCode = 403;
54
+ res.end(
55
+ JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Forbidden: non-loopback Host or Origin" }, id: null })
56
+ );
57
+ return;
58
+ }
59
+
51
60
  let body;
52
61
  if (req.method === "POST") {
53
62
  const chunks = [];
@@ -117,6 +126,24 @@ export async function startTransport(createMcpServer, env = process.env) {
117
126
  };
118
127
  }
119
128
 
129
+ // DNS-rebinding guard — the MCP spec requires a local HTTP server to validate Origin. Binding to
130
+ // 127.0.0.1 is not enough on its own: a web page whose hostname re-resolves to 127.0.0.1 reaches
131
+ // this port as its *own* origin, and every tool here runs in the user's logged-in Safari. MCP
132
+ // clients are not browsers: they send a loopback Host and no Origin. So the Host must name loopback
133
+ // on the port we are bound to, and an Origin, when a browser sends one, must name the same.
134
+ const LOOPBACK_HOST_PORT = /^(?:127\.0\.0\.1|localhost|\[::1\]):(\d+)$/i;
135
+ export function isLoopbackRequest(headers, port) {
136
+ const onOurPort = (hostPort) => Number(LOOPBACK_HOST_PORT.exec(hostPort ?? "")?.[1]) === port;
137
+ if (!onOurPort(headers.host)) return false;
138
+ if (headers.origin === undefined) return true;
139
+ try {
140
+ const { protocol, host } = new URL(headers.origin);
141
+ return (protocol === "http:" || protocol === "https:") && onOurPort(host);
142
+ } catch {
143
+ return false; // "null" and other opaque origins
144
+ }
145
+ }
146
+
120
147
  // An MCP initialize request may arrive as a single object or (rarely) batched in an array.
121
148
  function isInitialize(body) {
122
149
  const msgs = Array.isArray(body) ? body : [body];