Ariweb Suite 0.1.1 기반: 공통 라이브러리(db·http·세션·작업 엔진·할당/예약·알림·감사·보관소), 포털 뼈대, Webmaker/AriMail 도입 소스와 manifest
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
9ca4cee341
commit
c7aea8d419
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
# ssh 의 SSH_ASKPASS 헬퍼: 비밀번호를 명령행에 노출하지 않고 환경 변수로 전달한다.
|
||||
# (SSH_ASKPASS_REQUIRE=force 와 함께 사용 — OpenSSH 8.4+)
|
||||
printf '%s\n' "$WM_SSH_PASSWORD"
|
||||
Executable
+63
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
// ============================================================================
|
||||
// Claude Code PreToolUse 훅 (v1.6.31, 요구 3) — AI 작업을 사이트 폴더 안으로 가둔다. hooks/guard.sh 가 이 파일을 node 로 실행한다.
|
||||
//
|
||||
// 1.5.x 까지는 Write/Edit 계열의 file_path 만 검사해, Read/Glob/Grep 으로 다른 사이트·설정 파일·인증 정보를 읽거나 Bash 로 무엇이든 할 수 있었다(검토 S1).
|
||||
// 이제 다음을 막는다(차단 = 종료 코드 2, Claude 가 그 도구 호출을 거부한다):
|
||||
// - Read/Write/Edit/MultiEdit/NotebookEdit/Glob/Grep: 대상 경로가 SITE_DIR(실경로) 밖이거나 숨김 파일/폴더(.git·.claude 등; .refs 는 읽기만 허용)이거나 심볼릭 링크가 밖을 가리키면 차단
|
||||
// - Bash: 상위 경로(..), 절대 경로(/etc /proc /sys /root /home /var /srv /opt /usr/bin …), 네트워크·원격·프로세스·권한 명령(curl wget nc ssh scp sftp git systemctl kill sudo su chmod 777 …),
|
||||
// 환경·비밀 파일 이름(session.secret config.json users.json credentials vault setup.token .ssh .env), 셸 치환으로 경로를 숨기는 패턴($( ` ) 을 차단. cwd 밖으로 cd 금지
|
||||
// - WebFetch/WebSearch: 사설·루프백·링크로컬 IP, localhost, .local/.internal 호스트, http(s) 외 스킴 차단 (SSRF — 관리 화면·내부 서비스 접근)
|
||||
// 입력: stdin 에 Claude 가 주는 JSON { tool_name, tool_input }. 환경 SITE_DIR 은 lib/claude.js 가 넣는다.
|
||||
// ============================================================================
|
||||
const fs = require('fs'); const path = require('path'); const net = require('net');
|
||||
let input = ''; try { input = fs.readFileSync(0, 'utf8'); } catch (_) {}
|
||||
let payload = {}; try { payload = JSON.parse(input || '{}'); } catch (_) { process.stderr.write('guard: invalid JSON\n'); process.exit(2); }
|
||||
const tool = String(payload.tool_name || ''); const ti = payload.tool_input || {};
|
||||
const siteDir = process.env.SITE_DIR; if (!siteDir) { process.stderr.write('guard: SITE_DIR not set\n'); process.exit(2); }
|
||||
const siteReal = (() => { try { return fs.realpathSync(siteDir); } catch (_) { return path.resolve(siteDir); } })();
|
||||
const deny = (m) => { process.stderr.write(`Blocked by site guard: ${m}\n`); process.exit(2); };
|
||||
const insideSite = (p) => { const rel = path.relative(siteReal, p); return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel)); };
|
||||
/** 경로 하나 검사 (쓰기 도구는 숨김 전부 금지, 읽기 도구는 .refs 만 허용) */
|
||||
function checkPath(raw, write) {
|
||||
if (!raw) return;
|
||||
const abs = path.resolve(siteReal, String(raw));
|
||||
if (!insideSite(abs)) deny(`path outside the site folder: ${raw}`);
|
||||
const rel = path.relative(siteReal, abs); const parts = rel ? rel.split(path.sep) : [];
|
||||
if (parts.some((s, i) => s.startsWith('.') && !(i === 0 && s === '.refs' && !write))) deny(`hidden file/folder not allowed: ${raw}`);
|
||||
// 심볼릭 링크가 밖을 가리키면 차단 (존재하는 가장 깊은 조상까지 실경로 확인)
|
||||
let probe = abs; while (probe !== siteReal && !fs.existsSync(probe)) probe = path.dirname(probe);
|
||||
try { const real = fs.realpathSync(probe); if (!insideSite(real)) deny(`symlink escapes the site folder: ${raw}`); } catch (_) {}
|
||||
}
|
||||
if (['Write', 'Edit', 'MultiEdit', 'NotebookEdit'].includes(tool)) { checkPath(ti.file_path || ti.notebook_path, true); process.exit(0); }
|
||||
if (tool === 'Read') { checkPath(ti.file_path, false); process.exit(0); }
|
||||
if (tool === 'Glob' || tool === 'Grep') { if (ti.path) checkPath(ti.path, false); const pat = String(ti.pattern || ''); if (/^\/|^~|\.\.\//.test(pat)) deny(`pattern must stay inside the site folder: ${pat}`); process.exit(0); }
|
||||
if (tool === 'WebFetch' || tool === 'WebSearch') {
|
||||
const u = String(ti.url || ti.query || ''); if (tool === 'WebSearch') process.exit(0);
|
||||
let url; try { url = new URL(u); } catch (_) { deny(`invalid url: ${u}`); }
|
||||
if (!/^https?:$/.test(url.protocol)) deny(`scheme not allowed: ${url.protocol}`);
|
||||
const h = url.hostname.replace(/^\[|\]$/g, '').toLowerCase();
|
||||
if (h === 'localhost' || h.endsWith('.local') || h.endsWith('.internal') || h.endsWith('.localhost')) deny(`internal host not allowed: ${h}`);
|
||||
if (net.isIP(h)) { const v = net.isIP(h); const priv = v === 4 ? /^(0\.|10\.|127\.|169\.254\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.|22[4-9]\.|2[3-5]\d\.)/.test(h) : /^(::1$|::$|fc|fd|fe[89ab])/.test(h); if (priv) deny(`private address not allowed: ${h}`); }
|
||||
process.exit(0);
|
||||
}
|
||||
if (tool === 'Bash') {
|
||||
const cmd = String(ti.command || '');
|
||||
const bad = [
|
||||
[/(^|[\s;&|(`'"])\.\.(\/|$)/, 'parent directory reference'],
|
||||
[/(^|[\s=;&|(`'"])\/(etc|proc|sys|root|home|var|srv|opt|boot|dev|run|tmp|usr\/local|mnt|media)(\/|\s|$)/, 'absolute system path'],
|
||||
[/(^|[\s;&|(])(curl|wget|nc|ncat|netcat|ssh|scp|sftp|rsync|telnet|ftp|socat|nmap|dig|nslookup)(\s|$)/, 'network command'],
|
||||
[/(^|[\s;&|(])(sudo|su|chown|chgrp|mount|umount|systemctl|service|kill|pkill|killall|reboot|shutdown|crontab|at|nohup|setsid|docker|podman|certbot|nginx|apt|apt-get|dpkg|snap|pip|npm\s+(i|install|publish)|npx)(\s|$)/, 'privileged/process command'],
|
||||
[/(^|[\s;&|(])git\s+(push|remote|fetch|pull|clone|config)(\s|$)/, 'git remote operation'],
|
||||
[/(^|[\s;&|(])chmod\s+(-R\s+)?[0-7]*7[0-7]*(\s|$)/, 'world-writable chmod'],
|
||||
[/(^|[\s;&|(])cd\s+(\/|~|\.\.)/, 'cd outside the site folder'],
|
||||
[/\b(session\.secret|config\.json|users\.json|settings\.json|credentials|credentials\.json|vault|setup\.token|deploy-keys|deploy-targets|\.ssh|\.env|id_rsa|id_ed25519|authorized_keys|known_hosts)\b/i, 'sensitive file name'],
|
||||
[/\$\(|`|\$\{|<\(|>\(/, 'command substitution'],
|
||||
[/\b(env|printenv|export)\b\s*$/m, 'environment dump'],
|
||||
[/(^|\s)~\//, 'home directory path'],
|
||||
];
|
||||
for (const [re, why] of bad) if (re.test(cmd)) deny(`${why} in shell command`);
|
||||
process.exit(0);
|
||||
}
|
||||
process.exit(0);
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
# PreToolUse 훅 입구 (v1.6.31): 모든 검사는 hooks/guard.js(node) 가 한다. stdin 의 JSON 을 그대로 넘긴다. python 의존 제거.
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
exec node "$DIR/guard.js"
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Read|Write|Edit|MultiEdit|NotebookEdit|Glob|Grep|Bash|WebFetch|WebSearch",
|
||||
"hooks": [{ "type": "command", "command": "$HOOK_SCRIPT" }]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user