OILFIX 웹사이트 보안·성능 분석 보고서

비파괴 점검 결과 및 개선 지침 · 게시: https://oilfix-web.micfd.de/

점검 대상https://oilfix.github.io/
백엔드Cloudflare Worker + Google Apps Script + Google Sheets
점검일2026-09-23
점검 방식읽기 전용(비파괴), 운영 데이터 변경 없음
게시 서버micfd (216.23.93.119, Debian 12, Caddy v2.9.1)
게시 도메인oilfix-web.micfd.de (Let's Encrypt, 자동 갱신)

1. 요약

높음 3중간 4낮음 2확인 필요 4

2. 시스템 구성

방문자
 ├─ 정적 사이트 (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) — 정적 미러 + 본 보고서 게시

3. 보안 취약점 및 조치

높음S1. Worker 루트 응답의 내부 정보 노출

현상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/

추가: idOILFIX_SECRET에서 파생됐다면 wrangler secret put OILFIX_SECRET으로 교체(회전).

높음S2. STAFF 권한 과다 (최소 권한 위반)

현상 — 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('권한이 없습니다.');

높음S3. 로그인 계정 열거 + 무차별 대입 여지

현상 — 미등록/비활성 계정은 "등록되지 않았거나 비활성화된 직원 계정입니다.", 그 외에는 다른 오류로 구분됨. 시도 제한·잠금 없음(병렬 시도도 전부 처리됨). 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);

중간S4. 지원서 폼 스팸 가능

현상 — Apps Script URL이 공개되어 누구나 POST 가능(무제한).

조치 — Cloudflare Turnstile(무료) 또는 reCAPTCHA v3 도입 + Apps Script에서 토큰 검증. 최소 허니팟 필드 + IP당 제출 제한(1분 1회).

중간S5. 보안 헤더 부재 (GitHub Pages 한계)

현상 — 기존 사이트에 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"

중간S6. 검색엔진 노출 (robots.txt 없음)

조치ledger.html, apply.html<meta name="robots" content="noindex, nofollow"> 추가, 루트에 robots.txt:

User-agent: *
Allow: /
Disallow: /ledger.html
Disallow: /apply.html

중간S7. 공개 저장소를 통한 개인정보 노출

현상 — 저장소가 공개라 커밋 메타데이터에 작성자 이메일 herim2468007@gmail.com이 노출되고, 이 문자열이 Cloudflare 계정 서브도메인과 동일해 신원 연결이 가능.

조치

  1. GitHub → Settings → Emails → Keep my email addresses private + Block command line pushes that expose my email 활성화.
  2. 기존 이메일 제거: git filter-repo --email-callback 또는 BFG로 noreply 주소로 치환 후 force push.
  3. 소스 비공개를 원하면 자체 서버(micfd) 또는 Cloudflare Pages(비공개 저장소 지원)로 이전 후 GitHub Pages 비활성화.

낮음S8. 세션 토큰 정책 확인 필요

토큰을 sessionStorage에 보관(로컬스토리지보다 양호). 만료 시간·로그아웃 시 서버 무효화·PIN 변경 시 회전이 코드로 확인되지 않음.

낮음S9. 클라이언트 코드 전면 공개

함수명·인자 구조·요금 규칙이 전부 공개. 보안은 서버측 인증에 의존해야 하며, 클라이언트 검증(한도 등)은 우회 가능함을 전제로 서버에서 재검증해야 함.

확인 필요서버 코드에서 직접 확인할 항목

4. 성능 분석

측정 항목결과판정
홈페이지 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개선 필요

P1. 근본 원인 — Google Apps Script + Sheets를 실시간 API로 사용

모든 요청이 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대로 개선됩니다.

P2. 홈페이지가 방문마다 API를 호출 (캐시 무효화)

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 });

P3. 로그인 직후 동시 요청 폭주

ledger.jsafterLoginLoads()가 최대 9개 요청을 즉시 병렬 전송합니다. 관리자/매니저일수록 부하가 커져 502 확률이 오릅니다.

조치 — 화면에 보이는 섹션만 우선 로드하고, 나머지는 탭 진입·버튼 클릭 시 로드(레이지 로딩).

P4. 첫 방문 폰트 비용

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

P5. 저장소 용량 낭비

미사용 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

5. 조치 로드맵

우선순위항목난이도효과
P0 (즉시)S1 Worker 디버그 응답 제거 / 시크릿 확인매우 낮음정보 노출 차단
P0 (즉시)S3 로그인 메시지 통일 + 시도 제한 + PIN 해시낮음계정 탈취 방지
P0 (즉시)S2 역할별 bootstrap 분리 + 서버측 role 검사중간내부정보 보호
P1P2 캐시버스터 제거 + KV 캐시, P4 오류 정규화낮음홈페이지 안정화
P1S4 지원서 Turnstile 적용, S6 robots/noindex낮음스팸·노출 차단
P2P1 백엔드 D1/Supabase 이전, P3 레이지 로딩높음속도 근본 개선
P2S5 헤더 적용(자체 서버 이전), S7 저장소 정리, P5 폰트/용량중간마감·운영 품질

6. 배포·SSL 작업 내역 (완료)

참고: 이 미러에서 팀보드/장부 API는 CORS 미허용으로 동작하지 않습니다(Worker가 403 반환, 팀보드는 정적 값으로 폴백).