agent-dag 1.33.95 → 1.33.97

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.
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>agents-deck</title>
7
7
  <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='84' font-size='84'%3E%E2%97%89%3C/text%3E%3C/svg%3E" />
8
- <script type="module" crossorigin src="/assets/index-CdP7iiDC.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-mMOusrOQ.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-dwRx-2y0.css">
10
10
  </head>
11
11
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-dag",
3
- "version": "1.33.95",
3
+ "version": "1.33.97",
4
4
  "description": "Live deck of Claude Code and Codex agents — watch parallel subagents fork, call tools, and return on one calm canvas. Also available as npx ccdeck and npx agent-dag.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1696,16 +1696,55 @@ export function requestUrl(rawUrl) {
1696
1696
  // opaque `null` origin (sandboxed iframe, data: URL) fails
1697
1697
  // to parse and is refused with it.
1698
1698
  //
1699
- // A request carrying neither header is not a browser request and is allowed:
1700
- // that is hook/hook.js, a plain Node http.request from the user's own machine
1701
- // that sends no Origin at all, plus curl and the deck's own tooling. Ambient
1702
- // browser authority is the whole threat here, and those clients have none — a
1703
- // process that can POST here can already run anything as the user.
1699
+ // Agreement between the two is necessary but not sufficient, because both are
1700
+ // derived from the URL the page was served from and neither says a word about
1701
+ // the address the socket actually landed on — so the Host must also name a
1702
+ // loopback identity. See isLoopbackHost for the attack that gets through
1703
+ // without it.
1704
+ //
1705
+ // A request carrying neither header is not a browser request and is allowed
1706
+ // whatever Host it names: that is hook/hook.js, a plain Node http.request from
1707
+ // the user's own machine that sends no Origin at all, plus curl and the deck's
1708
+ // own tooling. Ambient browser authority is the whole threat here, and those
1709
+ // clients have none — a process that can POST here can already run anything as
1710
+ // the user, and nothing it sends is chosen by a page.
1704
1711
  export function isTrustedMutation({ origin, host, secFetchSite } = {}) {
1705
1712
  const site = typeof secFetchSite === "string" ? secFetchSite.trim().toLowerCase() : "";
1706
1713
  if (site && site !== "same-origin" && site !== "none") return false;
1707
- if (typeof origin !== "string" || origin === "") return true;
1708
- return originMatchesHost(origin, host);
1714
+ const hasOrigin = typeof origin === "string" && origin !== "";
1715
+ if (!hasOrigin && !site) return true;
1716
+ // Either header present means a browser sent this, so the rebinding gate
1717
+ // applies even to the shape that carries fetch metadata but no Origin.
1718
+ if (!isLoopbackHost(host)) return false;
1719
+ return hasOrigin ? originMatchesHost(origin, host) : true;
1720
+ }
1721
+
1722
+ // Was this request addressed to a name that can only ever be this machine?
1723
+ //
1724
+ // Origin === Host alone is not a defence against DNS rebinding: the page is
1725
+ // served from http://attacker.example:4317, the attacker re-points that record
1726
+ // at 127.0.0.1, and the browser then sends Host: attacker.example:4317 with a
1727
+ // matching Origin and Sec-Fetch-Site: same-origin — every header self-consistent
1728
+ // and every one attacker-chosen, because fetch metadata comes from the origin
1729
+ // tuple (scheme, host, port), not from the resolved IP. The browser also treats
1730
+ // the reply as same-origin, so the page reads the body: that is the account
1731
+ // share envelope, with the OAuth token in it. A loopback literal has no DNS
1732
+ // record to re-point, and `localhost` is reserved to loopback, so requiring one
1733
+ // of them is what separates the deck's own UI from the rebound page.
1734
+ function isLoopbackHost(host) {
1735
+ if (typeof host !== "string") return false;
1736
+ const authority = host.trim().toLowerCase();
1737
+ // A real Host header is bare authority. Userinfo or a path would let
1738
+ // `evil.example@127.0.0.1` and `127.0.0.1/…` parse to a loopback hostname
1739
+ // while naming something else, so refuse them rather than reason about them.
1740
+ if (authority === "" || /[/\\?#@\s]/.test(authority)) return false;
1741
+ let name;
1742
+ try { name = new URL(`http://${authority}`).hostname; } catch { return false; }
1743
+ if (name === "localhost" || name === "[::1]") return true;
1744
+ // The whole 127.0.0.0/8 is this machine, not just .0.1 — a second deck parked
1745
+ // on 127.0.0.2 is as local as the first. URL only produces a dotted quad for
1746
+ // something it already validated as an IPv4 address, so shape is enough here.
1747
+ return /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(name);
1709
1748
  }
1710
1749
 
1711
1750
  // Does `origin` name the same host:port the request was addressed to? Ports are