비파괴 점검 결과 및 개선 지침 · 게시: https://oilfix-web.micfd.de/
높음 3중간 4낮음 2확인 필요 4
방문자
├─ 정적 사이트 (HTML/CSS/JS/BGM)
│ GitHub Pages (저장소 oilfix/oilfix.github.io, 공개, Fastly CDN)
│
└─ 장부/팀보드 API POST {fn, args}
Cloudflare Worker oilfix-ledger-api.herim2468007.workers.dev
(OILFIX_SECRET 서명 후 중계, CORS는 https://oilfix.github.io 만 허용)
↓
Google Apps Script 웹앱 (비공개 주소)
↓
Google Sheets ← 실질 DB (거래·직원·후불·블랙리스트·근태·설정)
└ 지원서: 별도 Apps Script 웹앱 → '지원서' 시트 (Worker 미경유)
※ 신규: oilfix-web.micfd.de (micfd 서버, Caddy) — 정적 미러 + 본 보고서 게시
현상 — GET / 또는 임의의 ?public=... 값에 대해 아래 JSON이 반환됨.
{"ok":true,"service":"OILFIX LEDGER API V3",
"bindings":{"APPS_SCRIPT_URL":true,"OILFIX_SECRET":true},
"secret":{"length":64,"id":"720851499044"}}
영향 — 내부 구조·시크릿 존재 여부·길이, 그리고 12자리 식별자 노출. 시크릿 값 자체는 아니지만 공격 표면 정찰에 악용 가능. id가 시크릿 파생값이면 부분 유출.
조치
// Worker 라우팅 상단에서 디버그 응답 제거
if (request.method === 'GET') {
// 기존: bindings/secret 포함한 상세 응답 → 아래처럼 최소화
return Response.json({ ok: true }); // 또는 404
}
// wrangler.toml 배포 후 확인:
// curl https://oilfix-ledger-api.herim2468007.workers.dev/
추가: id가 OILFIX_SECRET에서 파생됐다면 wrangler secret put OILFIX_SECRET으로 교체(회전).
현상 — STAFF 계정 로그인 시 bootstrap 응답에 전 직원 11명의 이름·역할, 오늘 전체 매출·후불 현황, 최근 거래 8건(판매직원·고객 게임ID·닉네임·금액), 고객 통합 조회 결과까지 포함됨.
영향 — 업무상 불필요한 전사 데이터·개인정보 열람. 계정 1개 유출 시 내부 정보 전체 노출.
조치 — 역할별 응답 분리(서버측). 화면 숨김(data-role-min)만으로는 보안이 되지 않음.
// Apps Script: bootstrap 생성 시 역할 분기 예시
const rank = { STAFF:1, MANAGER:2, ADMIN:3 };
function buildBootstrap(user){
const meOnly = {
settings: publicSettings(),
customerTypes: TYPES,
dashboard: myDashboard(user.id) // 본인 실적만
};
if (rank[user.role] >= 2) Object.assign(meOnly, {
staff: staffList(), dashboard: teamDashboard(), recent: recentTx(20)
});
if (rank[user.role] === 3) Object.assign(meOnly, {
adminUsers: userList(), adminTeam: getTeam()
});
return meOnly;
}
// 각 api* 함수 첫 줄에서도 동일한 역할 검사 필수
if (rank[currentUser.role] < rank.MANAGER) throw new Error('권한이 없습니다.');
현상 — 미등록/비활성 계정은 "등록되지 않았거나 비활성화된 직원 계정입니다.", 그 외에는 다른 오류로 구분됨. 시도 제한·잠금 없음(병렬 시도도 전부 처리됨). PIN은 4~12자리 숫자.
영향 — 유효 계정 목록 확보 후 PIN 무차별 대입. 4자리는 1만 조합에 불과.
조치
// 1) 실패 메시지 통일
throw new Error('아이디 또는 PIN이 올바르지 않습니다.');
// 2) Worker KV 기반 시도 제한 (예시)
const key = `fail:${ip}:${loginId}`;
const n = Number(await KV.get(key) || 0);
if (n >= 5) return json({ ok:false, error:'잠시 후 다시 시도해주세요.' }, 429);
// 인증 실패 시: await KV.put(key, String(n+1), { expirationTtl: 600 });
// 인증 성공 시: await KV.delete(key);
현상 — Apps Script URL이 공개되어 누구나 POST 가능(무제한).
조치 — Cloudflare Turnstile(무료) 또는 reCAPTCHA v3 도입 + Apps Script에서 토큰 검증. 최소 허니팟 필드 + IP당 제출 제한(1분 1회).
현상 — 기존 사이트에 CSP·X-Frame-Options·HSTS 없음 → 로그인 페이지 클릭재킹 등.
조치 — GitHub Pages는 헤더 설정 불가. micfd.de(자체 서버)로 이전 시 Caddy에서 적용(본 보고서 사이트에는 이미 적용됨). 권장 CSP:
Content-Security-Policy "default-src 'self'; script-src 'self';
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src https://fonts.gstatic.com; img-src 'self' data:; media-src 'self';
connect-src 'self' https://oilfix-ledger-api.herim2468007.workers.dev
https://script.google.com;
frame-ancestors 'none'; base-uri 'self'; form-action 'self' https://script.google.com"
조치 — ledger.html, apply.html에 <meta name="robots" content="noindex, nofollow"> 추가, 루트에 robots.txt:
User-agent: *
Allow: /
Disallow: /ledger.html
Disallow: /apply.html
현상 — 저장소가 공개라 커밋 메타데이터에 작성자 이메일 herim2468007@gmail.com이 노출되고, 이 문자열이 Cloudflare 계정 서브도메인과 동일해 신원 연결이 가능.
조치
git filter-repo --email-callback 또는 BFG로 noreply 주소로 치환 후 force push.토큰을 sessionStorage에 보관(로컬스토리지보다 양호). 만료 시간·로그아웃 시 서버 무효화·PIN 변경 시 회전이 코드로 확인되지 않음.
함수명·인자 구조·요금 규칙이 전부 공개. 보안은 서버측 인증에 의존해야 하며, 클라이언트 검증(한도 등)은 우회 가능함을 전제로 서버에서 재검증해야 함.
api* 함수의 서버측 역할(role) 검증 여부 — 특히 관리자/정산/감사/계정 함수.| 측정 항목 | 결과 | 판정 |
|---|---|---|
| 홈페이지 TTFB (한국 기준) | 0.25~0.37초 (캐시 시 0.03초) | 양호 |
| 정적 자원 (CSS/JS) | 각 0.25초 내외, gzip 적용 | 양호 |
팀보드 API (?public=team) | 0.3초 ~ 30초+ 편차, 간헐 실패 | 불량 |
| 장부 로그인 1회 | 약 21초 | 불량 |
| 장부 동시 조회 | 34~37초 후 Google 오류 페이지(HTML) 502로 노출 | 불량 |
| 첫 방문 폰트 | CSS 857개 규칙(630KB, gzip 약 150KB, 렌더 블로킹) + 폰트 약 466KB | 개선 필요 |
모든 요청이 Worker → Apps Script → Sheets 3단 홉을 지나며, 스크립트 콜드스타트·시트 전체 스캔·동시성 잠금 때문에 요청당 수 초~수십 초가 소요됩니다. 동시 요청 시 Google 오류 HTML이 그대로 사용자에게 전달됩니다.
단기 — 자주 읽는 데이터(직급표, 대시보드)를 Worker KV에 30~60초 캐시. 오류 응답 정규화.
// Worker: Apps Script 오류를 사용자 친화 메시지로 변환
try { /* 프록시 */ }
catch (e) {
const msg = /<!DOCTYPE html>/.test(String(e)) || e.status >= 500
? '장부 서버가 혼잡합니다. 잠시 후 다시 시도해주세요.'
: e.message;
return Response.json({ ok:false, error: msg }, { status: 503 });
}
중기(권장) — 데이터 저장소를 Cloudflare D1 또는 Supabase(Postgres)로 이전. 시트는 백업/내보내기 용도로 유지. 로그인·세션·한도 검사도 D1로 옮기면 응답이 수십 ms대로 개선됩니다.
team-live.js가 ?_=${Date.now()}로 캐시를 강제 무효화해, 방문자 트래픽이 전부 Apps Script 쿼터를 소모합니다.
// team-live.js 45행 — 캐시버스터 제거
const res = await fetch(`${endpoint}/?public=team`, { cache: 'no-store' });
// → Worker에서 KV 캐시(60초) 적용을 권장. 예:
// const hit = await KV.get('team', 'json');
// if (hit) return Response.json({ ok:true, result:{ ok:true, team:hit } });
// ... Apps Script 조회 후 await KV.put('team', team, { expirationTtl: 60 });
ledger.js의 afterLoginLoads()가 최대 9개 요청을 즉시 병렬 전송합니다. 관리자/매니저일수록 부하가 커져 502 확률이 오릅니다.
조치 — 화면에 보이는 섹션만 우선 로드하고, 나머지는 탭 진입·버튼 클릭 시 로드(레이지 로딩).
Google Fonts 요청에 3개 서체·웨이트 6종(400~900)을 모두 포함 → @font-face 857개. 실제 사용 웨이트로 축소하거나 서브셋을 자체 호스팅하면 약 466KB → 100KB 이하로 감소.
<!-- 예: 400/700/900만 요청 -->
https://fonts.googleapis.com/css2?family=Black+Han+Sans&family=Montserrat:wght@700;900&family=Noto+Sans+KR:wght@400;700;900&display=swap
미사용 PNG 2개(2.87MB, 2.40MB)가 루트와 assets/에 중복(약 10MB). 페이지에서 참조되지 않으므로 삭제 권장.
git rm oilfix-logo-banner.png oilfix-logo-circle.png
git rm assets/oilfix-logo-banner.png assets/oilfix-logo-circle.png
| 우선순위 | 항목 | 난이도 | 효과 |
|---|---|---|---|
| P0 (즉시) | S1 Worker 디버그 응답 제거 / 시크릿 확인 | 매우 낮음 | 정보 노출 차단 |
| P0 (즉시) | S3 로그인 메시지 통일 + 시도 제한 + PIN 해시 | 낮음 | 계정 탈취 방지 |
| P0 (즉시) | S2 역할별 bootstrap 분리 + 서버측 role 검사 | 중간 | 내부정보 보호 |
| P1 | P2 캐시버스터 제거 + KV 캐시, P4 오류 정규화 | 낮음 | 홈페이지 안정화 |
| P1 | S4 지원서 Turnstile 적용, S6 robots/noindex | 낮음 | 스팸·노출 차단 |
| P2 | P1 백엔드 D1/Supabase 이전, P3 레이지 로딩 | 높음 | 속도 근본 개선 |
| P2 | S5 헤더 적용(자체 서버 이전), S7 저장소 정리, P5 폰트/용량 | 중간 | 마감·운영 품질 |
hk_micfd)/var/www/oilfix-web/ — 루트에 본 보고서, 정적 사이트 미러는 /site//etc/caddy/Caddyfile.bak.20260923222457참고: 이 미러에서 팀보드/장부 API는 CORS 미허용으로 동작하지 않습니다(Worker가 403 반환, 팀보드는 정적 값으로 폴백).