deel-local-cli 1.4.1 → 1.4.2

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.
package/README.en.md CHANGED
@@ -99,7 +99,7 @@ This page is the **summary**. Each section links to the detail behind it.
99
99
  | [Speed and spend](docs/en/tuning.md) | Per-stage effort · the prefix cache · context length |
100
100
  | [Safety and corporate review](docs/en/safety.md) | Undo · working scope · audit log · the review package |
101
101
  | [Configuration](docs/en/config.md) · [Development](docs/en/develop.md) | Env vars · run flags · running the tests · folder layout |
102
- | [Release notes](docs/en/releases.md) | [1.4.0](docs/en/releases.md#140) · [1.3.0](docs/en/releases.md#130) · [1.2.0](docs/en/releases.md#120) |
102
+ | [Release notes](docs/en/releases.md) | [1.4.2](docs/en/releases.md#142) · [1.4.1](docs/en/releases.md#141) · [1.4.0](docs/en/releases.md#140) · [1.3.0](docs/en/releases.md#130) · [1.2.0](docs/en/releases.md#120) |
103
103
 
104
104
  ---
105
105
 
@@ -893,7 +893,9 @@ so one run tells you everything.
893
893
 
894
894
  | Version | What changed |
895
895
  |---|---|
896
- | **[1.4.0](docs/en/releases.md#140)** | deel gets a face, speaks English, and sees meaning eleven places |
896
+ | **[1.4.2](docs/en/releases.md#142)** | 1.4.1 shipped before its own security fixes this corrects that |
897
+ | [1.4.1](docs/en/releases.md#141) | No new features, only what was actually found and fixed — Windows abort, ReDoS, XSS |
898
+ | [1.4.0](docs/en/releases.md#140) | deel gets a face, speaks English, and sees meaning — eleven places |
897
899
  | [1.3.0](docs/en/releases.md#130) | Evidence instead of claims, the editor instead of a terminal — six places |
898
900
  | [1.2.0](docs/en/releases.md#120) | So the conversation doesn't break — six places |
899
901
 
package/README.md CHANGED
@@ -100,7 +100,7 @@
100
100
  | [속도와 씀씀이](docs/ko/tuning.md) | 단계별 추론 강도 · 프리픽스 캐시 · 컨텍스트 길이 |
101
101
  | [안전망과 사내 반입](docs/ko/safety.md) | 되돌리기 · 작업 범위 · 감사기록 · 심사 서류 |
102
102
  | [설정](docs/ko/config.md) · [개발](docs/ko/develop.md) | 환경변수 · 실행 옵션 · 검사 돌리기 · 폴더 구조 |
103
- | [릴리스 노트](docs/ko/releases.md) | [1.4.0](docs/ko/releases.md#140) · [1.3.0](docs/ko/releases.md#130) · [1.2.0](docs/ko/releases.md#120) |
103
+ | [릴리스 노트](docs/ko/releases.md) | [1.4.2](docs/ko/releases.md#142) · [1.4.1](docs/ko/releases.md#141) · [1.4.0](docs/ko/releases.md#140) · [1.3.0](docs/ko/releases.md#130) · [1.2.0](docs/ko/releases.md#120) |
104
104
 
105
105
  ---
106
106
 
@@ -892,7 +892,9 @@ zip 은 진짜 `unzip` 으로, tar 는 진짜 `tar` 가 만든 것을 읽혀 교
892
892
 
893
893
  | 판 | 무엇이 바뀌었나 |
894
894
  |---|---|
895
- | **[1.4.0](docs/ko/releases.md#140)** | deel 얼굴을 갖고, 영어로도 말하고, 뜻까지 본다 열한 자리 |
895
+ | **[1.4.2](docs/ko/releases.md#142)** | 1.4.1보안 수정 전에 나갔다 그걸 바로잡는 |
896
+ | [1.4.1](docs/ko/releases.md#141) | 새 기능 없이, 실제로 찾은 것만 고친다 — 윈도우 abort·ReDoS·XSS |
897
+ | [1.4.0](docs/ko/releases.md#140) | deel 이 제 얼굴을 갖고, 영어로도 말하고, 뜻까지 본다 — 열한 자리 |
896
898
  | [1.3.0](docs/ko/releases.md#130) | 말 대신 증거, 터미널 대신 에디터 — 여섯 자리 |
897
899
  | [1.2.0](docs/ko/releases.md#120) | 대화가 끊기지 않게 — 여섯 자리 |
898
900
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deel-local-cli",
3
- "version": "1.4.1",
3
+ "version": "1.4.2",
4
4
  "description": "로컬 모델·사내 게이트웨이 전용 코딩 에이전트 CLI — 외부 의존성 0개 / Zero-dependency coding agent CLI for local LLMs and private gateways",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -60,12 +60,21 @@ const 되살림 = `
60
60
 
61
61
  const 안전한경로 = (u) => {
62
62
  // %00 이나 널바이트가 섞이면 아래 fs 가 경로를 잘라 읽는다. 통째로 거절한다.
63
+ // `\` 도 거절한다 — 앞의 `/` 는 지우지만 `\evil.com` 처럼 역슬래시로 시작하면
64
+ // 남아 있다가, 밑에서 `/${길}/` 로 리다이렉트 주소를 만들 때 일부 브라우저가
65
+ // `\` 를 `/` 로 봐서 `//evil.com/` 처럼 다른 도메인으로 새는 통로가 된다.
63
66
  let p;
64
67
  try { p = decodeURIComponent(String(u).split('?')[0].split('#')[0]); } catch { return null; }
65
- if (p.includes('\0')) return null;
68
+ if (p.includes('\0') || p.includes('\\')) return null;
66
69
  return p.replace(/^\/+/, '');
67
70
  };
68
71
 
72
+ /** 이용자 파일 이름을 HTML 안에 그대로 꽂기 전에 씌운다 — 파일 이름에
73
+ * `<script>` 가 들어 있어도 태그로 안 읽히게. */
74
+ const html씌우기 = (s) => String(s).replace(/[&<>"']/g, (c) => ({
75
+ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
76
+ }[c]));
77
+
69
78
  /**
70
79
  * 폴더 하나를 띄운다.
71
80
  *
@@ -294,15 +303,20 @@ function 목록주기(abs, 뿌리, res) {
294
303
  let 것들 = [];
295
304
  try { 것들 = readdirSync(abs, { withFileTypes: true }); } catch { /* 못 읽으면 빈 채로 */ }
296
305
  const 여기 = relative(뿌리, abs).split(sep).join('/');
306
+ // 파일 이름은 디스크에서 온 것이라 `<script>` 같은 것이 그대로 들어 있을 수
307
+ // 있다 — 씌우지 않고 꽂으면 그 파일이 있는 폴더를 미리보기로 여는 사람 화면에서
308
+ // 그대로 실행된다(Stored XSS). href 는 이미 encodeURIComponent 로 씌웠지만
309
+ // 화면에 보이는 글자는 안 씌웠던 것이 문제였다.
297
310
  const 줄 = (e) => {
298
- const 이름 = e.name + (e.isDirectory() ? '/' : '');
311
+ const 이름 = html씌우기(e.name) + (e.isDirectory() ? '/' : '');
299
312
  return `<li><a href="${encodeURIComponent(e.name)}${e.isDirectory() ? '/' : ''}">${이름}</a></li>`;
300
313
  };
301
- const = `<!doctype html><meta charset="utf-8"><title>${여기 || '/'}</title>`
314
+ const 여기씌움 = html씌우기(여기);
315
+ const 몸 = `<!doctype html><meta charset="utf-8"><title>${여기씌움 || '/'}</title>`
302
316
  + '<style>body{font:14px/1.7 ui-monospace,monospace;max-width:44rem;margin:3rem auto;padding:0 1rem}'
303
317
  + 'h1{font-size:1rem;color:#888;font-weight:400}a{color:#0a7}li{list-style:none}'
304
318
  + 'p{color:#999}@media(prefers-color-scheme:dark){body{background:#111;color:#ddd}a{color:#4d9}}</style>'
305
- + `<h1>/${여기}</h1>`
319
+ + `<h1>/${여기씌움}</h1>`
306
320
  + (여기 ? '<li><a href="../">../</a></li>' : '')
307
321
  + 것들.map(줄).join('')
308
322
  + '<p>index.html 이 없어서 목록을 보여 줍니다.</p>';
@@ -81,8 +81,11 @@ const BLOCKED = [
81
81
  { re: /\bdel\b[^|;&]*\s\/[sq]\b[^|;&]*(\\\*|\s[a-z]:\\?\s*$)/i, why: '하위 폴더까지 전부 지웁니다' },
82
82
  // 파워셸도 같은 일을 한다. 드라이브 뿌리나 집 폴더를 통째로 미는 것만 본다 —
83
83
  // 폴더 하나 지우는 평범한 Remove-Item 까지 막으면 도구를 못 쓴다.
84
+ // 가운데 `(-?[a-z]*\s*)*` 는 양쪽 다 빈 문자열을 허용해서, 안 맞는 긴 입력이
85
+ // 들어오면 되돌아가는 경우의 수가 기하급수로 늘어난다 — guard 검사 자체가
86
+ // 멈춰 버린다(ReDoS). 각 겹이 최소 한 글자는 먹게 고쳐서 그 길을 막는다.
84
87
  {
85
- re: /Remove-Item\b[^|;&]*\s-(Recurse|r)\b[^|;&]*\s(-?[a-z]*\s*)*([a-z]:\\?|~|\$HOME|\$env:USERPROFILE)\s*$/i,
88
+ re: /Remove-Item\b[^|;&]*\s-(Recurse|r)\b[^|;&]*\s(?:-?[a-z]+\s+)*([a-z]:\\?|~|\$HOME|\$env:USERPROFILE)\s*$/i,
86
89
  why: '드라이브나 집 폴더를 통째로 지웁니다',
87
90
  },
88
91
  { re: /git\s+push\b[^|;&]*--force(?!-with-lease)/i, why: '원격 이력을 덮어씁니다 (--force-with-lease 를 쓰세요)' },
@@ -91,15 +91,29 @@ function 얼마나쉬라나(머리, 회차) {
91
91
  }
92
92
 
93
93
  function 태그벗기기(html) {
94
- return html
95
- .replace(/<script[\s\S]*?<\/script>/gi, ' ')
96
- .replace(/<style[\s\S]*?<\/style>/gi, ' ')
97
- .replace(/<!--[\s\S]*?-->/g, ' ')
94
+ let 글 = String(html);
95
+ // script·style·주석을 겹쳐 쓰거나 안 닫은 모양으로 흘려 보내는 페이지가
96
+ // 있다 — 정규식 한 번으로는 다 못 걷어 낸다(파서가 아니라서). 더 지울 것이
97
+ // 없어질 때까지 반복한다. 그래도 이 글은 모델에게 주는 참고용 글일 뿐
98
+ // 화면에 그리지 않으니, 여기서 다 못 걸러도 실행되는 것은 아니다.
99
+ for (let i = 0; i < 5; i += 1) {
100
+ const 전 = 글;
101
+ 글 = 글
102
+ .replace(/<script[\s\S]*?<\/script\s*>/gi, ' ')
103
+ .replace(/<style[\s\S]*?<\/style\s*>/gi, ' ')
104
+ .replace(/<!--[\s\S]*?-->/g, ' ');
105
+ if (글 === 전) break;
106
+ }
107
+ return 글
98
108
  .replace(/<\/(p|div|section|article|li|tr|h[1-6]|br)>/gi, '\n')
99
109
  .replace(/<br\s*\/?>/gi, '\n')
100
110
  .replace(/<[^>]+>/g, ' ')
101
- .replace(/&nbsp;/g, ' ').replace(/&amp;/g, '&').replace(/&lt;/g, '<')
102
- .replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&#39;/g, "'")
111
+ // 번에 찾아서 한 번에 바꾼다 — 차례로 바꾸면(`&amp;` 먼저 `&` 로
112
+ // 풀고 나서 `&lt;` 를 다시 찾는 식) `&amp;lt;` 처럼 두 겹 씌운 것이
113
+ // 두 번 풀려서 `<` 로 튀어나온다(글자로 남아야 하는데 태그처럼 보이게 됨).
114
+ .replace(/&(nbsp|amp|lt|gt|quot|#39);/g, (_, 이름) => ({
115
+ nbsp: ' ', amp: '&', lt: '<', gt: '>', quot: '"', '#39': "'",
116
+ }[이름]))
103
117
  .replace(/[ \t]+/g, ' ')
104
118
  .replace(/\n\s*\n\s*\n+/g, '\n\n')
105
119
  .trim();
package/src/ui/status.js CHANGED
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // 남의 패키지를 붙이지 않는다. 필요한 숫자는 전부 session 이 이미 갖고 있고,
4
4
  // 화면 그리기는 ansi.js 만 쓴다. 반입 심사에 새로 설명할 것이 늘지 않게 하려는 뜻이다.
5
- import { c, gauge, 눈금게이지, width, clip, cols, mark } from './ansi.js';
5
+ import { c, 눈금게이지, width, clip, cols, mark } from './ansi.js';
6
6
  import { PROFILES } from '../agent/effort.js';
7
7
  import { get as workMode, canWrite, 보일이름 } from '../agent/modes.js';
8
8
  import { isLocalHost, isOffline } from '../safety/network.js';