TOOLS GAMES DEVLOG DOCS ABOUT CONTACT
HOME > DOCS > Web / Tools > 백엔드 없는 정적 사이트 Q&A 보드: Firestore 보안 규칙과 Web Crypto SHA-256Serverless Static Site Q&A Board: Firestore Security Rules and Web Crypto SHA-256
Web / Tools Advanced Firebase Firestore / Web Crypto API

백엔드 없는 정적 사이트 Q&A 보드: Firestore 보안 규칙과 Web Crypto SHA-256Serverless Static Site Q&A Board: Firestore Security Rules and Web Crypto SHA-256

별도 WAS 서버 없이 정적 호스팅 환경에서 Web Crypto API(SHA-256) 비밀번호 해싱, Cloudflare Turnstile 봇 방지 위젯, Firestore 스키마 검증 규칙을 결합해 댓글 보드를 구축하는 아키텍처입니다.Architecting a serverless comment board on static hosting by combining Web Crypto API (SHA-256) password hashing, Cloudflare Turnstile bot protection, and strict Firestore schema validation rules.

#Firestore #Security #WebCrypto #Turnstile #Serverless
핵심 개념정적 웹사이트(GitHub Pages, Cloudflare Pages)에서 게임별 Q&A나 피드백 게시판을 운영할 때 별도의 백엔드 서버를 두면 유지비와 관리 포인트가 늘어납니다. 브라우저 내장 Web Crypto API(crypto.subtle.digest)로 비밀번호를 64자 SHA-256 해시로 변환하고, Firestore Security Rules에서 필드 타입·길이·서버 타임스탬프 일치 여부를 엄격히 검증하면 서버리스 환경에서도 안전한 쓰기 전용 게시판을 구성할 수 있습니다.
Key ConceptRunning a dedicated backend server just to host a game Q&A or feedback board on a static website (GitHub Pages, Cloudflare Pages) introduces unnecessary maintenance and hosting costs. By hashing passwords into 64-character SHA-256 hex strings via the browser's native Web Crypto API (crypto.subtle.digest) and enforcing strict field types, string lengths, and server timestamps in Firestore Security Rules, you can deploy a secure append-only feedback board without a backend.

1. Web Crypto SHA-256 해싱 및 Firestore 스키마 검증 규칙 구현1. Implementing Web Crypto SHA-256 Hashing and Firestore Schema Rules

클라이언트에서는 평문 비밀번호를 네트워크로 전송하기 전에 SHA-256 16진수 문자열(64바이트)로 해싱하고, Firestore 보안 규칙(firestore.rules)에서는 허용된 5개 필드 외의 임의 필드 주입과 위변조된 타임스탬프를 차단합니다.

On the client side, hash plaintext passwords into 64-character SHA-256 hexadecimal strings before network transmission. In firestore.rules, reject any payload containing unauthorized fields or forged client timestamps.

qna_client_and_rules.js
// --- [1. 브라우저 Web Crypto API 기반 SHA-256 해시 생성] ---
async function sha256Hex(plainText) {
    const encoder = new TextEncoder();
    const data = encoder.encode(plainText);
    const hashBuffer = await crypto.subtle.digest('SHA-256', data);
    const hashArray = Array.from(new Uint8Array(hashBuffer));
    return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}

// 댓글 문서 생성 페이로드 구성
async function submitComment(db, pageId, nickname, password, bodyText, turnstileToken) {
    if (!turnstileToken) throw new Error('봇 방지 인증을 완료해 주세요.');
    const passwordHash = await sha256Hex(password);

    await addDoc(collection(db, 'comments'), {
        pageId: pageId.slice(0, 200),
        nickname: nickname.trim().slice(0, 20),
        passwordHash: passwordHash, // 길이 64자의 16진수 문자열
        body: bodyText.trim().slice(0, 1000),
        createdAt: serverTimestamp() // request.time 과 일치 강제
    });
}

/* --- [2. Firestore Security Rules (firestore.rules)] ---
service cloud.firestore {
  match /databases/{database}/documents {
    match /comments/{commentId} {
      allow read: if true;
      allow create: if isValidComment();
      allow update, delete: if false;
    }
    function isValidComment() {
      let d = request.resource.data;
      let k = d.keys();
      return k.hasAll(['pageId', 'nickname', 'passwordHash', 'body', 'createdAt'])
        && k.hasOnly(['pageId', 'nickname', 'passwordHash', 'body', 'createdAt'])
        && d.pageId is string && d.pageId.size() > 0 && d.pageId.size() <= 200
        && d.nickname is string && d.nickname.size() > 0 && d.nickname.size() <= 20
        && d.passwordHash is string && d.passwordHash.size() == 64
        && d.body is string && d.body.size() > 0 && d.body.size() <= 1000
        && d.createdAt == request.time;
    }
  }
}
*/
qna_client_and_rules.js
// --- [1. Browser Web Crypto API SHA-256 Hash Generation] ---
async function sha256Hex(plainText) {
    const encoder = new TextEncoder();
    const data = encoder.encode(plainText);
    const hashBuffer = await crypto.subtle.digest('SHA-256', data);
    const hashArray = Array.from(new Uint8Array(hashBuffer));
    return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}

// Construct and submit comment document payload
async function submitComment(db, pageId, nickname, password, bodyText, turnstileToken) {
    if (!turnstileToken) throw new Error('Please complete the bot verification check.');
    const passwordHash = await sha256Hex(password);

    await addDoc(collection(db, 'comments'), {
        pageId: pageId.slice(0, 200),
        nickname: nickname.trim().slice(0, 20),
        passwordHash: passwordHash, // Fixed 64-character hex string
        body: bodyText.trim().slice(0, 1000),
        createdAt: serverTimestamp() // Enforced to match request.time
    });
}

/* --- [2. Firestore Security Rules (firestore.rules)] ---
service cloud.firestore {
  match /databases/{database}/documents {
    match /comments/{commentId} {
      allow read: if true;
      allow create: if isValidComment();
      allow update, delete: if false;
    }
    function isValidComment() {
      let d = request.resource.data;
      let k = d.keys();
      return k.hasAll(['pageId', 'nickname', 'passwordHash', 'body', 'createdAt'])
        && k.hasOnly(['pageId', 'nickname', 'passwordHash', 'body', 'createdAt'])
        && d.pageId is string && d.pageId.size() > 0 && d.pageId.size() <= 200
        && d.nickname is string && d.nickname.size() > 0 && d.nickname.size() <= 20
        && d.passwordHash is string && d.passwordHash.size() == 64
        && d.body is string && d.body.size() > 0 && d.body.size() <= 1000
        && d.createdAt == request.time;
    }
  }
}
*/

2. 댓글 문서 스키마 및 보안 검증 조건2. Comment Document Schema and Security Constraints

좌우로 밀어서 표 전체 내용을 확인하세요
필드명데이터 타입Firestore 규칙 제약 조건보안 설계 목적
pageIdstringsize() > 0 && size() <= 200게임 페이지별 댓글 격리 및 과도한 키 길이 차단
nicknamestringsize() > 0 && size() <= 20작성자 표시 이름 길이 제한으로 UI 레이아웃 깨짐 방지
passwordHashstringsize() == 64평문 비밀번호 저장 방지 (SHA-256 Hex 64바이트 고정 규격 강제)
bodystringsize() > 0 && size() <= 1000대용량 텍스트 도배를 통한 스토리지 과금 공격 방지
createdAttimestampd.createdAt == request.time클라이언트의 과거/미래 시간 조작 차단 (서버 타임스탬프 강제)
Swipe horizontally to view the full table
Field NameData TypeFirestore Rule ConstraintSecurity Purpose
pageIdstringsize() > 0 && size() <= 200Isolates comments per game page and prevents oversized key strings
nicknamestringsize() > 0 && size() <= 20Caps author display name length to prevent UI layout overflow
passwordHashstringsize() == 64Prevents plaintext password storage (enforces 64-byte SHA-256 hex format)
bodystringsize() > 0 && size() <= 1000Mitigates storage billing abuse from oversized text payloads
createdAttimestampd.createdAt == request.timePrevents client-side timestamp spoofing by requiring server time

3. 실무 적용 및 주의사항3. Practical Considerations and Caveats

  • DOM 렌더링 시 XSS 방어: Firestore에서 읽어온 nickname과 body를 화면에 표시할 때는 절대 innerHTML에 직접 넣지 말고, textContent를 사용하거나 HTML 엔티티 이스케이프 함수를 거쳐야 저장형 XSS(Stored XSS)를 막을 수 있습니다.
  • 클라이언트 캡차의 한계와 보완: 백엔드 없이 브라우저에서만 Turnstile 위젯 성공 여부를 확인하는 방식은 일반 UI 봇은 막아주지만, Firestore REST 엔드포인트로 직접 패킷을 쏘는 공격까지 차단하지는 못하므로 트래픽 증가 시 Firebase App Check 또는 Cloudflare Worker 검증을 연동해야 합니다.
  • 수정·삭제 권한 분리: 익명 게시판 특성상 클라이언트에서의 update, delete를 보안 규칙에서 false로 잠그면 타인의 댓글 훼손을 근본적으로 방지할 수 있습니다.
  • Prevent Stored XSS on Render: When rendering nickname and body fetched from Firestore, always assign them via textContent or an HTML entity escaping helper rather than raw innerHTML.
  • Client-Side Captcha Limitations: Checking Turnstile widget state on the client blocks automated browser bots, but direct REST requests to Firestore require enabling Firebase App Check or verifying tokens via a Cloudflare Worker.
  • Lock Down Update and Delete Permissions: Setting allow update, delete: if false; in Firestore rules ensures existing comments cannot be tampered with or deleted from the client.

Web Crypto API 해싱과 Firestore의 선언적 스키마 검증 규칙을 결합하면, 별도의 데이터베이스 서버를 운영하지 않고도 정적 사이트에서 가볍고 안전한 사용자 피드백 창구를 유지할 수 있습니다.

Combining Web Crypto API hashing with declarative Firestore schema validation rules allows static sites to host a lightweight, low-maintenance user Q&A board without a dedicated backend.