Compare commits

..
Author SHA1 Message Date
DemonGatanjieuandClaude Opus 5.5 aa9c237f98 Only this computer can change things; other devices can look (v1.58.1)
With ComfyUI started with --listen, other devices could use most of AMB's
actions: delete and rename models, save and delete workflows and prompts,
start downloads and scans, export a backup of all data, translate with the
user's key, change settings, switch the plugin version. A probe of all
routes as a LAN device found 9 of 112 refusing.

api/local_guard.py is one middleware in front of every /anomalous/ route:
anything but GET answers only this computer unless it is a read sent as POST
(OPEN_POSTS); a GET is open unless it is more than a read (LOCAL_GETS:
backup file, GitHub releases, local folders). Local = loopback peer, no
proxy headers, loopback Host (DNS rebinding is refused too). Refusals are
403 local_only with a bilingual message; GET /anomalous/access lets the page
show a "view only" chip (remote_view.js). AGENTS.md rule 4 now names it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 01:38:20 +08:00
12 changed files with 158 additions and 5 deletions
+1 -1
View File
@@ -108,7 +108,7 @@ ComfyUI-Manager 收录审核检查的就是这几类口子(2026-10 审 Anomalo
1. **不执行下载来的或用户文件里的代码。** 数据不用 `pickle`、`eval`、`exec`、动态导入读;配置文件当数据解析。模型文件只读 safetensors / GGUF 的文件头,`.ckpt` / `.pt` / `.pth` / `.bin` 不打开,界面提醒它们可能带代码。
2. **下载要能核对。** 只从允许的 https 站点下载,先写 `.part`,核对 SHA-256 后才改名放入;没有可核对的指纹时在界面上说明。下载的东西只当数据用。
3. **接口不接受任意本机路径。** 只收“类型 + 根目录序号 + 相对路径”,在后端用 `resolve_within` 校验;需要指向本机某个文件时,由服务端自己找到它,只给页面一个令牌。
4. **会写入、移动、删除文件或替用户联网的接口只接受本机请求**(请求来自本机,没有经过代理,Host 也是本机)。用 `--listen` 开放到局域网时,别的设备只能查看。新接口必须做到;已有接口还没全部做到,改到哪个补哪个。
4. **会写入、移动、删除文件或替用户联网的接口只接受本机请求**(请求来自本机,没有经过代理,Host 也是本机)。用 `--listen` 开放到局域网时,别的设备只能查看。由 `api/local_guard.py` 的中间件统一把关:`/anomalous/` 下 GET 以外的请求默认只接受本机,确实只读的 POST 列进 `OPEN_POSTS`,不只是读的 GET(备份文件、联网、本机文件夹)列进 `LOCAL_GETS`;新接口要想清楚属于哪一类,不要为了方便放进 `OPEN_POSTS`。
5. **用户的文件删除一律进回收站,没有回收站就拒绝;从不覆盖**(同名另存为“名字 (2)”或拒绝)。插件自己产生的临时文件(`.part`、暂存区)可以直接删。
6. **只读的接口也按第 3 条来。** “只是读”不是例外;能按名字、编号取的,就不收路径,由服务端在自己登记的范围里找到文件。
7. **读用户文件要有上限。** 限制读取的大小和条数;读不了或超大的文件跳过并在结果里说明,不让一个坏文件拖垮整个请求。读到的内容进页面只当纯文本(本节第二条)。
+4 -1
View File
@@ -111,6 +111,8 @@ covered by the `styles.css` manifest.
lifecycle; `api/materials.py` owns HTTP mapping and compatibility entry points.
`api/node_material.py` saves one canvas node's current values as a
`node_parameter_selection` material (the current-node panel's "save").
- `api/local_guard.py` decides which requests only this computer may make (every change under
`/anomalous/`; other devices may look), as one middleware; see backend.md.
- `api/media_routes.py`, `api/gallery_routes.py`, `api/translation_routes.py`, and
`api/folder_types.py` own the formerly mixed utility route families.
- `model_policies.py` owns shared backend rename and protected-category policy.
@@ -294,7 +296,8 @@ covered by the `styles.css` manifest.
seeking, search, paging, download and deletion, showing the speech/voice/seed
recorded in each file, plus a section for unsaved temp previews that can be
copied into `output/audio/`.
- `ui_sidebar.js` assembles the browser window (rail, list column, header, page panels)
- `ui_sidebar.js` assembles the browser window (rail, list column, header, page panels;
`remote_view.js` adds the header's "view only" chip when opened from another device)
and renders the model folder list. `ui_shell_rail.js` owns the left icon rail: one
entry per page, the tool slots and the settings slot. `ui_shell_nav.js` owns page
navigation (`goTo`): the domain each page needs, which pages have a list and whether
+7
View File
@@ -1,5 +1,12 @@
# 📈 Anomalous Model Browser Changelog
## v1.58.1 Beta (Security: only this computer can change things) — 2026-10-12
### 🔒 Other devices can look, not change (别的设备只能看,不能改)
- When ComfyUI is started with `--listen`, other devices on your network can open it. Until now they could also use most of Anomalous Model Browser's actions: delete or rename models, save and delete workflows, combos and prompts, start downloads and scans, export a backup of all your data, translate with your translation key, change settings, switch the plugin's version. Now every change answers only the computer ComfyUI runs on; other devices can still browse models, pictures and workflows.
- A window opened from another device shows **View only** next to the page title; pressing a button that changes something there says why it did nothing.
- A web page that reaches ComfyUI through another name for this computer, or a proxy, counts as another device too.
## v1.58.0 Beta (New Layout, Keep & Reuse, Prompt Studio, Model Check, Voices) — 2026-10-09
### 🧭 New layout (新布局)
+1 -1
View File
@@ -9,7 +9,7 @@
[![ComfyUI Manager](https://img.shields.io/badge/ComfyUI-Manager-green?style=for-the-badge&logo=comfyui)](https://github.com/ltdrdata/ComfyUI-Manager)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow?style=for-the-badge)](LICENSE)
[![Changelog](https://img.shields.io/badge/📖_Changelog-v1.58.0_Beta-blue?style=for-the-badge)](CHANGELOG.md)
[![Changelog](https://img.shields.io/badge/📖_Changelog-v1.58.1_Beta-blue?style=for-the-badge)](CHANGELOG.md)
[![Bilibili Video](https://img.shields.io/badge/Bilibili-视频演示-00A1D6?style=for-the-badge&logo=bilibili)](https://www.bilibili.com/video/BV1a1bv68EuA/)
[![YouTube Video](https://img.shields.io/badge/YouTube-Video_Demo-red?style=for-the-badge&logo=youtube)](https://youtu.be/hAvsj7uiaCw)
+5 -1
View File
@@ -1,7 +1,7 @@
from . import (
activity_log, audio_catalog, folder_types, gallery_routes, materials, media_routes, model_catalog,
model_media, model_metadata, model_resolution, model_type_listing, kept_images, node_material, recipe_packages, recipes, scan_report, scan_summary,
translation_routes, version_manager,
local_guard, translation_routes, version_manager,
)
from .scanner import *
from .config import *
@@ -29,6 +29,10 @@ async def no_cache_extension_middleware(request, handler):
def setup_routes(app):
if hasattr(app, 'middlewares') and no_cache_extension_middleware not in app.middlewares:
app.middlewares.append(no_cache_extension_middleware)
# Other devices on the network may look; only this computer may change things (local_guard.py).
if hasattr(app, 'middlewares') and local_guard.local_only_middleware not in app.middlewares:
app.middlewares.append(local_guard.local_only_middleware)
app.router.add_get('/anomalous/access', local_guard.api_access)
app.router.add_get('/anomalous/folders', model_catalog.api_get_folders)
app.router.add_get('/anomalous/all_folder_types', folder_types.api_get_all_folder_types)
app.router.add_get('/anomalous/models', model_catalog.api_get_models)
+82
View File
@@ -0,0 +1,82 @@
"""What only this computer may do. ComfyUI started with --listen lets other devices on the
network open the page: they may look (lists, searches, pictures, covers), but every change
under /anomalous/ (writing, moving and deleting files, downloads, scans, translations, backups,
version switches, saved settings) answers only requests from the computer ComfyUI runs on
(AGENTS.md 安全底线, rule 4). One middleware decides it for all routes: a request other than
GET is local-only unless it is a read sent as POST (OPEN_POSTS); a GET is open unless it is not
a plain read (LOCAL_GETS). A request is local when it comes from a loopback address, through no
proxy, naming a loopback host; checking the Host also keeps out a page that reached this server
under another name (DNS rebinding)."""
import ipaddress
import urllib.parse
from aiohttp import web
PREFIX = "/anomalous/"
PROXY_HEADERS = ("Forwarded", "X-Forwarded-For", "X-Real-IP", "X-Forwarded-Host")
LOCAL_NAMES = ("localhost",)
# Reads sent as POST (their lists are long): open to other devices too. They write nothing.
OPEN_POSTS = frozenset({
"/anomalous/resolve_paths_to_previews",
"/anomalous/resolve_hash_batch",
"/anomalous/inspect_image_material",
})
# GETs that are more than a read: a backup of all your data, a call to GitHub, your folders.
LOCAL_GETS = frozenset({
"/anomalous/backup/file",
"/anomalous/version/releases",
"/anomalous/import/folders",
})
# Routes that answer other devices themselves (MCP replies in JSON-RPC).
OWN_CHECK = frozenset({"/anomalous/mcp"})
READS = ("GET", "HEAD", "OPTIONS")
MESSAGE = ("只能在运行 ComfyUI 的电脑上操作,别的设备只能浏览。"
" Only the computer ComfyUI runs on can do this; other devices can only look.")
def loopback_host(name):
if not name:
return False
name = name.strip("[]").lower()
if name in LOCAL_NAMES:
return True
try:
return ipaddress.ip_address(name).is_loopback
except ValueError:
return False
def is_local(request):
"""The request comes from this computer, directly, and names a loopback host."""
if not loopback_host(request.remote or ""):
return False
if any(header in request.headers for header in PROXY_HEADERS):
return False
return loopback_host(urllib.parse.urlsplit("//" + request.headers.get("Host", "")).hostname)
def needs_local(method, path):
"""Whether a request to `path` with `method` must come from this computer."""
if not path.startswith(PREFIX) or path in OWN_CHECK:
return False
if method.upper() in READS:
return path in LOCAL_GETS
return path not in OPEN_POSTS
def refusal():
return web.json_response({"status": "error", "success": False, "error": "local_only", "code": "local_only",
"message": MESSAGE}, status=403)
@web.middleware
async def local_only_middleware(request, handler):
if needs_local(request.method, request.path) and not is_local(request):
return refusal()
return await handler(request)
async def api_access(request):
"""GET /anomalous/access - whether this page may change things: {"local": bool}."""
return web.json_response({"local": is_local(request)})
+14
View File
@@ -40,6 +40,20 @@ inventory recognizes GGUF and PTH case-insensitively in addition to the existing
list formats. This route-level inventory does not expand global model-format,
scanner, metadata-parser, recovery, or cloud-identification support.
## Requests from other devices
ComfyUI started with `--listen` lets other devices on the network open the page. They may
look; every change under `/anomalous/` answers only the computer ComfyUI runs on. One
middleware (`api/local_guard.py`, registered first in `setup_routes`) decides it for all routes:
a request other than GET must be local unless its path is in `OPEN_POSTS` (reads sent as POST:
`resolve_paths_to_previews`, `resolve_hash_batch`, `inspect_image_material`); a GET is open
unless its path is in `LOCAL_GETS` (`backup/file`, `version/releases`, `import/folders`).
`/anomalous/mcp` answers other devices itself. Local means: a loopback peer, no proxy headers
(`Forwarded`, `X-Forwarded-For`, `X-Real-IP`, `X-Forwarded-Host`) and a loopback `Host`, so a
page that reached the server under another name (DNS rebinding) is refused too. A refusal is
HTTP 403 with `code: local_only` and a bilingual message. `GET /anomalous/access` tells the page
whether it is local; `remote_view.js` then shows a "view only" chip in the header.
## Storage ownership
Runtime UI settings and newly stored API keys live in `api/config.json`.
+4
View File
@@ -571,6 +571,8 @@ export const i18n = {
detailBackAssistant: '⬅ 返回助手面板',
detailJumpToBottom: '跳到底部',
detailEdit: '编辑',
remoteViewChip: '仅浏览',
remoteViewHint: '这个窗口是从别的设备打开的:可以浏览;删除、移动、下载、扫描、保存这类改动,只能在运行 ComfyUI 的电脑上做。',
detailNotesTitle: '📓 专属备注 (Notes)',
detailEditNotes: '编辑备注',
detailGeneratedGallery: '查看历史生成作品',
@@ -2467,6 +2469,8 @@ export const i18n = {
detailBackAssistant: '⬅ Back to Assistant',
detailJumpToBottom: 'Jump to bottom',
detailEdit: 'Edit',
remoteViewChip: 'View only',
remoteViewHint: 'This window was opened from another device: you can look around; deleting, moving, downloading, scanning, saving and other changes can only be made on the computer ComfyUI runs on.',
detailNotesTitle: '📓 Custom Notes',
detailEditNotes: 'Edit Notes',
detailGeneratedGallery: 'View Generated Gallery',
+24
View File
@@ -0,0 +1,24 @@
/**
* Opened from another device (ComfyUI started with --listen): a chip in the header says this
* window can look but not change anything (the server refuses changes from other devices,
* api/local_guard.py). Nothing shows on the computer ComfyUI runs on.
*/
import { translate as t } from './locales.js';
/** Asks the server once whether this page is on its computer; when not, marks the window and adds the chip to `group`. */
export async function markRemoteView(container, group) {
try {
const response = await fetch('/anomalous/access', { cache: 'no-store' });
const data = response.ok ? await response.json() : null;
if (!data || data.local !== false) return;
} catch {
return; // an older server without the route: nothing to say
}
container.classList.add('anomalous-remote-view');
const chip = document.createElement('span');
chip.className = 'anomalous-remote-chip';
chip.textContent = t('remoteViewChip');
chip.title = t('remoteViewHint');
group.append(chip);
}
+2
View File
@@ -3,6 +3,7 @@
* The browser window (rail, list column, header, page panels) and the model folder list.
*/
import { markRemoteView } from './remote_view.js';
import { translate } from './locales.js';
import { escapeHtml } from './safe_dom.js';
import { showUpdateGuide } from './ui_update_guide.js';
@@ -157,6 +158,7 @@ export function createDOM() {
rightGroup.append(updateNoticeBtn, dockBtn, closeBtn);
header.append(leftGroup, centerGroup, rightGroup);
markRemoteView(container, leftGroup); // opened from another device: "view only"
this.grid = panel('anomalous-grid', '');
this.detailPanel = panel('anomalous-detail');
+1 -1
View File
@@ -13,7 +13,7 @@
@import url("./styles/12-tts-setup.css?v=20261002-theme-1");
@import url("./styles/13-script-director.css?v=20261002-voiceover-2");
@import url("./styles/14-version-manager.css?v=20261002-theme-1");
@import url("./styles/15-shell-rail.css?v=20261003-feedback-2");
@import url("./styles/15-shell-rail.css?v=20261012-remote");
@import url("./styles/16-activity.css?v=20261002-theme-1");
@import url("./styles/17-model-types.css?v=20261002-theme-1");
@import url("./styles/18-scan-page.css?v=20261003-folders-1");
+13
View File
@@ -517,3 +517,16 @@
color: var(--amb-btn-primary-text);
font-weight: 600;
}
/* Opened from another device (remote_view.js): a quiet "view only" chip beside the page title. */
.anomalous-remote-chip {
flex-shrink: 0;
margin-left: 8px;
padding: 2px 9px;
border: 1px dashed var(--amb-border-strong);
border-radius: 999px;
color: var(--amb-text-muted);
font-size: 11.5px;
white-space: nowrap;
cursor: help;
}