DeepSeek Harness Remote
DeepSeek Harness Remote: DeepSeek Harness 실행 PC와 모바일·PC를 안전하게 연결해 어디서나 작업 이어받기
저장소 정보는 아직 수집하지 않았습니다.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 README에서 읽을 수 있습니다. 저장소 README ↗
README 전체를 불러오는 중입니다.
README 전문 보기 ↗이 저장소는 1단계에서 학생 본인이 GitHub 저장소와 Vercel 배포를 만드는 출발점입니다. 포함된 메모 네 건은 가상 자료입니다. 실제 학생 자료, 토큰, 비밀키를 넣지 마세요.
https://…vercel.app 주소를 방어전 1단계 카드에 붙여넣고 제출합니다. 저장소 주소나 설정 파일은 적지 않습니다.배포가 끝나면 /에서 점령된 가상 자료실을 볼 수 있습니다. /data.json에는 같은 가상 메모가 공개됩니다. 이 공개 상태를 확인하는 것이 1단계의 출발점입니다. 1단계 접수와 심판 판정은 포털에서 확인합니다.
vercel.json은 정적 결과물 public과 api 폴더의 서버 함수를 배포하고, 2단계부터 모든 응답에 보안 헤더 X-Content-Type-Options: nosniff를 붙입니다. 빌드 명령 npm run build는 aleph.config.json의 step(1~12)을 확인하고, Vercel이 제공하는 GitHub 저장소 소유자·이름, 커밋 SHA, 배포 URL을 검증해 public/aleph.json을 생성합니다. 2단계부터 빌드는 public/data.json을 만들지 않습니다. 이 값이 없으면 빌드가 실패하므로, 성공한 것처럼 빈 주소를 내보내지 않습니다. aleph.json의 내용만으로 저장소 소유권이나 방어 성공을 인정하지 않습니다. 심판이 공개 저장소의 실제 커밋과 배포된 자료를 따로 대조해야 합니다.
aleph.config.json의 repoUrl과 publicAppUrl은 1단계에서 자리표시자였고 학생이 편집하지 않습니다. 2단계 저장점부터 실제 GitHub 저장소와 Vercel Production 주소가 들어 있습니다. 2단계 이후 코딩 도구가 필요한 설정과 보호 기능을 단계별로 작성합니다. npm run bundle과 bundle-notes.json도 1단계의 세 걸음에는 포함되지 않습니다.
로컬에서 가상 화면만 확인할 때는 npm run build -- --local을 사용합니다. 로컬 실행은 Vercel 배포나 심판 접수를 증명하지 않습니다. 저장소의 src/attack-check.mjs는 실제 배포 주소에 비로그인 요청을 보냅니다. 1단계에서는 /data.json의 공개 가상 메모 확인 표시를 읽었고, 2단계부터는 /data.json과 /api/notes에서 보이는 가상 메모 수를 셉니다. 3단계부터는 무로그인·가짜 토큰으로 GET·POST·PUT·DELETE를 보내 거부되는지 기록합니다. 학생 계정이 필요한 정상 A·B 요청은 미실행으로 남깁니다.
public.notes에 있습니다. 테이블은 RLS가 켜져 있고 정책이 없어서, anon·authenticated는 직접 읽을 수 없습니다. owner_id uuid 칸은 이후 단계에서 쓰려고 미리 만들어 두었습니다./)은 서버 함수 api/notes.js(GET /api/notes)를 거쳐 메모를 읽습니다. 함수는 Vercel 환경변수 SUPABASE_URL과 서버 전용 SUPABASE_SECRET_KEY를 씁니다. 두 값은 Vercel 대시보드 → Settings → Environment Variables에 직접 넣습니다. 코드·Git·브라우저 파일·응답·로그에는 넣지 않습니다./data.json과 Git의 공개 파일에는 메모가 없습니다.supabase/notes.sql)은 가상 메모 문장을 담고 있어 .gitignore로 로컬에만 둡니다.aleph.config.json의 repoUrl과 publicAppUrl에 실제 GitHub 저장소와 Vercel Production 주소를 적습니다.npm run bundle(먼저 bundle-notes.json의 explanation을 적습니다): src/attack-check.mjs가 배포 주소에 비로그인 요청을 보냅니다. 정상이면 /data.json은 메모 0건이고, /api/notes는 메모 4건으로 남은 약점이 기록됩니다. 이 결과는 학생의 자기 점검이며 심판 판정이 아닙니다.남은 약점: /api/notes는 아직 로그인 확인이 없는 공개 주소입니다. 서버 전용 키는 숨겨졌지만, 이 주소를 아는 누구나 메모 네 건을 그대로 받을 수 있습니다. 3단계에서 로그인 확인으로 막아야 합니다.
검색식은 실습용 가상 (과제|포트폴리오|리추얼|행정) 기록입니다. 이 README 자체는 이 검색식에 걸리지 않습니다. <배포주소>에는 Vercel Settings → Domains의 Production 주소를 넣습니다. 배포마다 생기는 주소는 배포 보호 때문에 302가 나올 수 있습니다.
GitHub 최신 파일 검색 (저장소 폴더의 Git Bash):
git fetch origin && git grep -nE "실습용 가상 (과제|포트폴리오|리추얼|행정) 기록" origin/main
정상: 아무것도 출력되지 않습니다.
현재 배포의 정적 파일 검색:
for p in / /data.json /aleph.json; do echo "$p $(curl -s -o /dev/null -w '%{http_code}' https://<배포주소>$p) $(curl -s https://<배포주소>$p | grep -oE '실습용 가상 (과제|포트폴리오|리추얼|행정) 기록' | wc -l)"; done
정상: /과 /aleph.json은 200 0, /data.json은 404 0입니다.
공개 API 약점 확인 (약점 기록용이며, 정적 파일 검색과 따로 적습니다):
curl -s https://<배포주소>/api/notes | grep -oE '실습용 가상 (과제|포트폴리오|리추얼|행정) 기록' | wc -l
현재 결과는 4여야 합니다. 이 숫자는 로그인 없이 메모를 받을 수 있다는 약점을 뜻합니다.
| 항목 | 결과 | 확인한 사람·날짜 |
|---|---|---|
GitHub 최신 파일(origin/main = 8d3a0b1) | 0건 | 코딩 도구가 로컬에서 git grep 실행, 2026-10-26 |
2단계 당시 배포 /, /data.json, /aleph.json | / 200·0건(X-Content-Type-Options: nosniff), /data.json 404·0건, /aleph.json 200·0건 (step 2, 커밋 8d3a0b1) | 코딩 도구가 Production 주소에 비로그인 요청, 2026-10-26 |
2단계 당시 공개 API /api/notes 약점 | 200·4건: 로그인 없이 메모 네 건을 받을 수 있음 (3단계에서 401로 막음) | 코딩 도구가 Production 주소에 비로그인 요청, 2026-10-26 |
과거 노출은 해소되지 않았습니다. 첫 공개 커밋 89a0dfc의 data.json과 public/data.json에는 메모 네 건이 그대로 남아 있습니다. GitHub에서 이 커밋을 누구나 열 수 있습니다. 그 커밋으로 만든 옛 Vercel 배포도 남아 있다면 /data.json에서 메모가 보일 수 있습니다. 최신 파일 검색 결과가 0건이라는 것은 "지금 파일에 없다"는 뜻일 뿐, 이미 공개됐던 기록이 지워졌다는 뜻이 아닙니다.
/ 화면은 Supabase Auth 이메일·비밀번호 로그인·로그아웃을 공식 SDK(@supabase/supabase-js의 signInWithPassword·signOut)로 처리했습니다. 3단계 당시 화면 코드에는 공개용 Project URL과 publishable key만 들어 있었고, 5단계에서 로그인을 서버 함수로 옮기며 화면에서 지웠습니다. 로그인에 실패하면 이유를 화면에 보여 줍니다.src/verify-login.mjs로 Authorization: Bearer 토큰을 검사합니다. 토큰이 없거나 검사에 실패하면 자료 없이 401 LOGIN_REQUIRED를 돌려줍니다. 브라우저가 보낸 userId·role·owner_id는 읽지 않습니다. 검사에 쓴 발급자 정보는 aleph.config.json의 identityProvider에 있습니다.aleph.config.json의 allowedRoutes와 같습니다.| 경로 | 동작 |
|---|---|
GET /api/notes | 서버가 확인한 사용자의 메모 [{id,title,body}] |
POST /api/notes | {id?,title,body}, id(UUID)가 없으면 서버가 만들어 201 {id}, owner_id는 확인된 사용자 ID |
GET /api/notes/:id | {id,title,body}, 없으면 404 |
PUT /api/notes/:id | {title,body}로 고친 뒤 {id,title,body}, 없으면 404 |
DELETE /api/notes/:id | 204, 지운 뒤 GET은 404 |
supabase/notes.sql(로컬 전용) 다음에 supabase/notes-step3.sql을 실행합니다. 이 SQL은 id를 UUID로, content를 body로 바꾸고 RLS와 권한 회수를 유지합니다. 그다음 Supabase Authentication → Users에서 A 계정을 만들고(Auto Confirm), 배포 주소에서 로그인합니다./api/notes)은 막혔습니다. 3단계 저장점의 자기 점검에서 무로그인과 가짜 토큰으로 보낸 GET·POST·PUT·DELETE 요청이 모두 자료 없이 401이었습니다.npm run bundle은 무로그인·가짜 토큰 요청만 보냅니다. 정상 A의 추가·수정·삭제는 학생 비밀번호를 쓰지 않으려고 자동 점검에서 보내지 않으며, 결과에는 미실행으로 남습니다. 이 결과는 학생의 자기 점검이며 심판 판정이 아닙니다./api/notes/:id의 GET·PUT·DELETE는 로그인만 확인하고 메모 주인은 검사하지 않았습니다. 4단계에서 owner_id 검사로 막았습니다.owner_id가 비어 있었습니다. 4단계에서 학생이 SQL로 A에게 연결합니다.GET /api/notes는 owner_id가 같은 메모만 돌려주고, POST는 그 ID를 owner_id로 저장합니다. GET·PUT·DELETE /api/notes/:id는 id와 owner_id가 모두 맞는 행만 다룹니다. 남의 메모와 없는 메모는 똑같이 404 NOTE_NOT_FOUND입니다.owner_id 조건이 맡습니다.auth.users에서 A·B의 ID를 찾아, 기존 가상 메모 네 건의 owner_id를 A로 연결하고 B 소유 시험 메모 한 건을 넣습니다.public.notes에 RLS 최소 권한을 둡니다. PUBLIC·anon·authenticated 권한을 모두 회수한 뒤 authenticated에만 SELECT·INSERT·UPDATE·DELETE를 줍니다. 정책 네 개(notes_select_own, notes_insert_own, notes_update_own, notes_delete_own)는 모두 auth.uid() = owner_id일 때만 허용합니다. 이 정책은 publishable key로 DB에 직접 들어오는 요청을 막습니다.pg_policies, information_schema.role_table_grants, has_table_privilege로 확인합니다.src/verify-login.mjs로 검사): B가 A 메모를 읽기·수정·삭제하면 모두 404였고 A 메모는 그대로였습니다. A가 B 메모를 읽거나 지울 때도 404였고, 각자 자기 메모는 추가·수정·삭제할 수 있었습니다. 이 결과는 실제 배포의 A·B 계정 시험이 아닙니다.npm run bundle의 A·B 교차 요청은 학생 비밀번호를 쓰지 않으려고 보내지 않으며, 결과에는 미실행으로 남습니다. 화면에서 직접 확인합니다.POST /api/notes에 남의 메모 id를 넣으면 409 NOTE_ID_TAKEN이 나옵니다. 내용은 보이지 않지만, 그 id가 있다는 사실은 알 수 있습니다.89a0dfc과 옛 배포의 과거 노출은 그대로입니다.public/index.html)는 메모 자료를 Supabase에서 직접 읽거나 고치지 않습니다. 5단계에서 찾아본 결과, supabase.from·/rest/v1 같은 직접 자료 호출은 없었고 모든 자료 요청은 서버 함수 /api/notes, /api/notes/:id로 갑니다. 당시 화면의 Supabase 호출은 로그인(supabase.auth)뿐이었습니다./에서 sb_publishable_ 0건).
POST /api/auth/login: 서버가 공식 SDK의 signInWithPassword를 부릅니다. 받은 토큰은 응답 본문에 넣지 않고 HttpOnly; Secure; SameSite=Strict 쿠키(vault_access, vault_refresh)로만 보냅니다. 실패하면 INVALID_CREDENTIALS 같은 이유를 JSON으로 돌려줍니다.GET /api/auth/session: 쿠키의 토큰을 Supabase Auth로 확인해 이메일만 돌려줍니다. 만료됐으면 갱신 토큰으로 새 토큰을 받습니다.POST /api/auth/logout: Supabase 세션을 끝내고 쿠키를 지웁니다.src/auth-session.mjs에 있습니다. 로그인용 Supabase 클라이언트는 요청마다 새로 만들어, 사용자 세션이 자료용 서버 클라이언트에 섞이지 않게 합니다.Authorization: Bearer 머리글(심판·도구)과 쿠키(화면)의 토큰을 같은 src/verify-login.mjs로 검사합니다. 쿠키로 보낸 로그인·로그아웃·쓰기 요청은 Origin이 같은 출처일 때만 받습니다(CSRF 방어). 다르거나 없으면 403 CROSS_SITE_REQUEST입니다.owner_id)와 서버 전용 설정(SUPABASE_URL, SUPABASE_SECRET_KEY)은 4단계 그대로입니다. 로그인도 이 두 값만 쓰며, 새 환경변수는 없습니다.246908e) 뒤 실제 주소에서 확인한 결과: 없는 계정 로그인은 401 INVALID_CREDENTIALS, 다른 출처에서 보낸 로그인은 403, 무로그인 세션·목록과 가짜 쿠키 목록은 모두 401 JSON이었습니다. 실제 A 계정 로그인과 메모 CRUD는 자동 점검으로 보내지 않으며 학생이 화면에서 확인합니다.aleph.config.json의 originalApiUrl(https://xvttauolkmecdscuthnb.supabase.co/rest/v1/notes, 쿼리 없음)입니다. 심판은 anon 키로 이 주소에 직접 요청해 확인합니다.public.notes의 PUBLIC·anon·authenticated 테이블·칸 권한을 모두 회수하는 SQL을 제안했습니다. RLS와 4단계 정책은 남겨 두고, 서버 함수가 쓰는 service_role과 다른 테이블은 건드리지 않습니다. 적용 전후 information_schema.role_table_grants, has_table_privilege, information_schema.column_privileges로 대조합니다. SQL 적용 여부는 코딩 도구가 확인하지 못했습니다.npm run bundle 결과에 기록됩니다. 화면에 키가 없으므로 공개용 키는 실행할 때 환경변수로 넘깁니다(Git Bash: SUPABASE_PUBLISHABLE_KEY=sb_publishable_… npm run bundle). 넘기지 않으면 이 두 점검은 미실행으로 남습니다. 서버 전용 키(sb_secret_)를 넣으면 점검 코드가 거부합니다.POST의 남의 id에 대한 409 응답, 옛 커밋 89a0dfc의 과거 노출은 4단계와 같이 남아 있습니다.noreply 주소로 기록하며, 이미 남은 기록은 고쳐 쓰지 않았습니다.xdr/fixtures/brute-force.json(수업용 Wazuh 모양 경보 28건, 원본은 고치지 않음)을 읽어 판단합니다.
| 파일 | 하는 일 |
|---|---|
xdr/brute-force/patterns.json | MITRE ATT&CK T1110 근거 패턴 두 개: same-source-failure-burst(T1110.001, 같은 주소의 짧은 시간 로그인 실패 연속), password-spraying(T1110.003, 여러 계정에 같은 비밀번호 대입) |
xdr/brute-force/decide.mjs | 심판이 격리 환경에서 불러오는 판정기. import·파일 읽기·바깥 요청 없이 위 패턴을 맨 위 상수로 옮겨 적고 decide(alert)가 {action, confidence, reason}을 돌려줍니다. 확신도 0.85 이상은 block, 0.5 이상은 alert, 그 아래는 record입니다. T1110 표시가 없으면 0.1, 있으면 0.5에서 시작해 실패 건수(2 |
xdr/brute-force/respond.mjs | decide 결과 중 block의 출발 주소만 거부 후보로 xdr/brute-force/deny-rules.json에 씁니다(만료 시각 = 처리 시각 + 60분, 근거 경보 번호 포함). 계정 이름으로는 규칙을 만들지 않고, 정상(record) 이벤트에 나온 주소는 뺍니다. block·alert는 xdr/alerts.log에 한 줄씩 쌓습니다. |
xdr/brute-force/read-alerts.mjs | 확인용. 시각·출발 주소·계정·규칙 수준·설명만 뽑고 비밀값처럼 보이는 값은 가립니다. decide.mjs가 불러오지 않습니다. |
npm run xdr:run -- brute-force(결과 xdr/brute-force/result.json), node xdr/brute-force/respond.mjs(거부 후보와 알림), node xdr/brute-force/read-alerts.mjs(경보 확인).npm run xdr:run -- brute-force 결과: block 10, alert 9, record 9. T1110 표시가 없는 정상 이벤트 9건은 모두 record였습니다. 이 결과는 학생의 로컬 실행이며, 심판이 격리 환경에서 decide.mjs를 다시 실행해 판정합니다.src/decider.mjs)가 집행하지 않습니다. 판정기 요청 계약(docs/DECIDER_REQUEST.md)에는 출발 주소가 없고(signals.source는 실제 경로에서 none) 사용자는 가명 subjectId로만 와서, 후보를 실제 요청에 맞춰 볼 값이 없습니다. 판정기의 기존 규칙(starter.deny)은 바꾸지 않았습니다. 로컬에서 후보 규칙에 시험 경보를 다시 대어 보면 명확한 공격 10건만 막히고 애매한 시도·정상 이벤트 18건은 통과했습니다.xdr/fixtures/web-injection.json(수업용 Wazuh 모양 경보 26건, 원본은 고치지 않음)을 읽어 판단합니다.
| 파일 | 하는 일 |
|---|---|
xdr/web-injection/patterns.json | MITRE ATT&CK T1190 근거 패턴 세 개: sql-injection-in-params(요청 인자 안의 SQL 구문, CWE-89), script-tag-in-params(스크립트 태그, CWE-79), path-traversal-repeat(경로 거슬러 올라가기 반복, CWE-22) |
xdr/web-injection/decide.mjs | 심판이 격리 환경에서 불러오는 판정기. import·파일 읽기·바깥 요청 없이 위 패턴을 맨 위 상수로 옮겨 적고 decide(alert)가 {action, confidence, reason}을 돌려줍니다. 확신도 0.85 이상은 block, 0.5 이상은 alert, 그 아래는 record입니다. T1190 표시나 패턴 일치가 없으면 0.1, 있으면 0.5에서 시작해 같은 출발 주소(data.srcip)의 반복 횟수(1~8번)만큼 0.95까지 올립니다. 출발 주소가 없으면 반복을 세지 않습니다. 숫자 기준은 MITRE에 없어 연습용으로 정한 값입니다. |
xdr/web-injection/respond.mjs | decide 결과 중 block의 출발 주소만 거부 후보로 xdr/web-injection/deny-rules.json에 씁니다(만료 시각 = 처리 시각 + 60분, 근거 경보 번호 포함). 계정 이름으로는 규칙을 만들지 않고, 정상(record) 이벤트에 나온 주소는 뺍니다. block·alert는 xdr/alerts.log에 한 줄씩 쌓고, 시험 경보를 후보 규칙에 다시 흘려 막힘·통과를 셉니다. |
xdr/web-injection/read-alerts.mjs | 확인용. 시각·출발 주소·계정·규칙 수준·설명만 뽑고 비밀값처럼 보이는 값은 가립니다. 요청 주소는 뽑지 않습니다. decide.mjs가 불러오지 않습니다. |
npm run xdr:run -- web-injection(결과 xdr/web-injection/result.json, 성공하면 화면 출력 없음), node xdr/web-injection/respond.mjs(거부 후보·알림·다시 흘리기), node xdr/web-injection/read-alerts.mjs(경보 확인).npm run xdr:run -- web-injection 결과: block 8, alert 9, record 9. T1190 표시가 없는 정상 이벤트 9건은 모두 record였습니다. 이 결과는 학생의 로컬 실행이며, 심판이 격리 환경에서 decide.mjs를 다시 실행해 판정합니다.block 8건만 막혔고, alert 9건과 record 9건은 통과했습니다.decide.mjs를 push하기 전에 제출해 심판이 파일을 찾지 못했습니다. 두 번째 제출은 X02_CLEAR_NOT_BLOCKED였습니다. wi-06(명령 구분자 표기 11번 반복)을 세 패턴에 맞지 않는다는 이유로 alert로 낮췄고, 출발 주소를 확인하지 않았기 때문입니다. 지금은 T1190 표시와 같은 주소의 반복으로 block합니다.wi-06의 차단 근거는 세 패턴이 아니라 T1190 표시와 반복입니다. 명령 주입 패턴(CWE-78)은 patterns.json에 없습니다.src/decider.mjs)가 집행하지 않습니다. 판정기 요청 계약에는 출발 주소가 없고 쿼리도 길이·지문으로만 옵니다. 판정기의 기존 규칙(starter.deny)은 바꾸지 않았습니다.AGENTS.md를 먼저 읽히고 한 번에 한 제작 단위만 요청하세요. 3단계 이후의 로그인, 허용 경로, 5단계의 원본 API 주소, 6단계 이후 정책 규칙은 해당 단계 원고와 계약에 맞춰 추가합니다. 비밀번호·토큰·서버 전용 키·실제 학생 기록을 코드, Git, 제출 묶음에 넣지 않습니다.
src/decider.mjs와 src/detect.mjs의 로컬 시험은 반 엔진이나 운영 심판의 결과가 아닙니다. 1단계 이후 제출 묶음 계약 aleph.defense.submission.v2는 scripts/bundle.mjs에 남아 있으며, 코딩 도구가 해당 단계의 최신 배포 주소와 Git 원격을 맞춘 뒤 사용합니다.
aleph.config.json을 GitHub에서 로그인 없이 읽습니다. 저장소가 비공개였을 때는 값이 있어도 originalApiUrl이 없다고 판정됐고, 저장소를 Public으로 바꾸자 통과했습니다.