July 19, 2025

πŸ”‘ ν΄λΌμ΄μ–ΈνŠΈ μ½”λ“œμ— λ―Όκ°ν•œ API ν‚€ 직접 λ…ΈμΆœ: λ³΄μ•ˆ 침해와 μ„œλΉ„μŠ€ λ‚¨μš©μ˜ 지름길

React
λ³΄μ•ˆ
μ•„ν‚€ν…μ²˜
λΉŒλ“œ&λ²ˆλ“€λ§

Summary

ν΄λΌμ΄μ–ΈνŠΈ μ‚¬μ΄λ“œ μ½”λ“œμ— API ν‚€λ‚˜ λ―Όκ°ν•œ μ„€μ • 정보λ₯Ό 직접 ν¬ν•¨ν•˜λŠ” 것은 μ‹¬κ°ν•œ λ³΄μ•ˆ μ·¨μ•½μ μž…λ‹ˆλ‹€. κ³΅κ²©μžκ°€ 이λ₯Ό νƒˆμ·¨ν•˜μ—¬ μ„œλΉ„μŠ€ λ‚¨μš©, 데이터 유좜 λ“±μ˜ λ¬Έμ œκ°€ λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€. κ°€μž₯ μ•ˆμ „ν•œ 방법은 λ°±μ—”λ“œ ν”„λ‘μ‹œ μ„œλ²„λ₯Ό 톡해 ν‚€λ₯Ό κ΄€λ¦¬ν•˜λŠ” 것이며, ν΄λΌμ΄μ–ΈνŠΈ λ²ˆλ“€μ— λ…ΈμΆœλ  μˆ˜λ°–μ— μ—†λŠ” 곡개 ν‚€μ˜ κ²½μš°μ—λ„ ν™˜κ²½ λ³€μˆ˜λ₯Ό μ‚¬μš©ν•˜κ³  ν•΄λ‹Ή μ„œλΉ„μŠ€μ˜ λ³΄μ•ˆ 섀정을 ν™œμš©ν•˜μ—¬ μ˜€μš©μ„ λ°©μ§€ν•΄μ•Ό ν•©λ‹ˆλ‹€.

Why Wrong?

ν΄λΌμ΄μ–ΈνŠΈ μ‚¬μ΄λ“œ μ½”λ“œ(JavaScript λ²ˆλ“€, HTML λ“±)λŠ” μ‚¬μš©μžμ˜ λΈŒλΌμš°μ €μ—μ„œ μ‹€ν–‰λ˜λ©°, 개발자 도ꡬλ₯Ό 톡해 λˆ„κ΅¬λ‚˜ μ‰½κ²Œ μ ‘κ·Όν•˜μ—¬ λ‚΄μš©μ„ λΆ„μ„ν•˜κ³  λ””μ»΄νŒŒμΌν•  수 μžˆμŠ΅λ‹ˆλ‹€. 여기에 API ν‚€λ‚˜ 인증 토큰, λ―Όκ°ν•œ μ„€μ • κ°’ 등을 직접 λ…ΈμΆœν•˜λŠ” 것은 λ‹€μŒκ³Ό 같은 μ‹¬κ°ν•œ λ³΄μ•ˆ 취약점을 μ•ΌκΈ°ν•©λ‹ˆλ‹€.

  • λ³΄μ•ˆ 취약점: μ•…μ˜μ μΈ μ‚¬μš©μžκ°€ μ½”λ“œλ₯Ό λΆ„μ„ν•˜μ—¬ λ―Όκ°ν•œ API ν‚€λ₯Ό νƒˆμ·¨ν•  수 μžˆμŠ΅λ‹ˆλ‹€. μ΄λŠ” ν•΄λ‹Ή API μ„œλΉ„μŠ€μ— λ¬΄λ‹¨μœΌλ‘œ μ ‘κ·Όν•˜κ±°λ‚˜ μ˜€μš©ν•  수 μžˆλŠ” ν†΅λ‘œκ°€ λ©λ‹ˆλ‹€.
  • μ„œλΉ„μŠ€ λ‚¨μš© 및 λΉ„μš© λ°œμƒ: νƒˆμ·¨λœ API ν‚€λŠ” 주둜 μ™ΈλΆ€ μ„œλΉ„μŠ€(지도 API, 결제 API, AI API λ“±)의 ν• λ‹ΉλŸ‰μ„ μ†Œλͺ¨ν•˜κ±°λ‚˜, κ³΅κ²©μžκ°€ 자체적으둜 κ°œλ°œν•œ μ•…μ„± μ„œλΉ„μŠ€μ— μ—°λ™ν•˜μ—¬ κΈˆμ „μ  ν”Όν•΄λ₯Ό μž…νž 수 μžˆμŠ΅λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, 지도 API ν‚€κ°€ λ…ΈμΆœλ˜λ©΄ λ¬΄λ‹¨μœΌλ‘œ κ³Όλ„ν•œ μš”μ²­μ„ λ°œμƒμ‹œμΌœ λΆˆν•„μš”ν•œ μ‚¬μš©λ£Œλ₯Ό 청ꡬ당할 수 μžˆμŠ΅λ‹ˆλ‹€.
  • 데이터 유좜 μœ„ν—˜: λ―Όκ°ν•œ API ν‚€κ°€ λ°μ΄ν„°λ² μ΄μŠ€λ‚˜ λ‹€λ₯Έ λ°±μ—”λ“œ μ„œλΉ„μŠ€μ™€ μ—°λ™λ˜μ–΄ μžˆλ‹€λ©΄, ν‚€ νƒˆμ·¨λŠ” κ³§ μ€‘μš”ν•œ 데이터 유좜의 ν†΅λ‘œκ°€ 될 수 μžˆμŠ΅λ‹ˆλ‹€.
  • μœ μ§€λ³΄μˆ˜ 어렀움: 개발, ν…ŒμŠ€νŠΈ, ν”„λ‘œλ•μ…˜ ν™˜κ²½λ§ˆλ‹€ λ‹€λ₯Έ ν‚€λ₯Ό κ΄€λ¦¬ν•˜κΈ° μ–΄λ ΅κ³ , ν‚€ λ³€κ²½ μ‹œλ§ˆλ‹€ μ½”λ“œ 전체λ₯Ό μž¬λ°°ν¬ν•΄μ•Ό ν•˜λŠ” λ²ˆκ±°λ‘œμ›€μ΄ λ°œμƒν•©λ‹ˆλ‹€.

How to Fix?

λ―Όκ°ν•œ API ν‚€λŠ” μ ˆλŒ€ ν΄λΌμ΄μ–ΈνŠΈ λ²ˆλ“€μ— 직접 ν¬ν•¨ν•΄μ„œλŠ” μ•ˆ λ©λ‹ˆλ‹€. λ‹€μŒ 방법을 톡해 μ•ˆμ „ν•˜κ²Œ 관리해야 ν•©λ‹ˆλ‹€.

  • λ°±μ—”λ“œ ν”„λ‘μ‹œ μ„œλ²„ ν™œμš© (κ°€μž₯ ꢌμž₯): κ°€μž₯ κ°•λ ₯ν•œ λ³΄μ•ˆ λ°©λ²•μž…λ‹ˆλ‹€. λ―Όκ°ν•œ API ν‚€κ°€ ν•„μš”ν•œ λͺ¨λ“  μš”μ²­μ€ ν”„λŸ°νŠΈμ—”λ“œμ—μ„œ 직접 μ™ΈλΆ€ API μ„œλΉ„μŠ€λ‘œ λ³΄λ‚΄λŠ” λŒ€μ‹ , κ°œλ°œμžκ°€ ν†΅μ œν•˜λŠ” λ°±μ—”λ“œ μ„œλ²„(ν”„λ‘μ‹œ)λ₯Ό 거쳐 λ°±μ—”λ“œμ—μ„œ API ν‚€λ₯Ό μ‚¬μš©ν•˜μ—¬ μ‹€μ œ μ™ΈλΆ€ API μ„œλΉ„μŠ€μ— μš”μ²­μ„ λ³΄λƒ…λ‹ˆλ‹€. λ°±μ—”λ“œ μ„œλ²„λŠ” API ν‚€λ₯Ό ν™˜κ²½ λ³€μˆ˜λ‘œ μ•ˆμ „ν•˜κ²Œ κ΄€λ¦¬ν•˜λ©°, ν΄λΌμ΄μ–ΈνŠΈλŠ” 였직 λ°±μ—”λ“œ ν”„λ‘μ‹œ μ„œλ²„μ™€ ν†΅μ‹ ν•©λ‹ˆλ‹€. 이 방식은 API ν‚€κ°€ ν΄λΌμ΄μ–ΈνŠΈμ—κ²Œ μ „ν˜€ λ…ΈμΆœλ˜μ§€ μ•Šλ„λ‘ ν•©λ‹ˆλ‹€.

  • ν™˜κ²½ λ³€μˆ˜ μ‚¬μš© (λΉŒλ“œ μ‹œ μ£Όμž…, 곡개 킀에 ν•œν•¨): μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ„ λΉŒλ“œν•  λ•Œ .env 파일 등을 톡해 ν™˜κ²½ λ³€μˆ˜λ₯Ό μ£Όμž…ν•©λ‹ˆλ‹€. 이 방법은 λΉŒλ“œλœ ν΄λΌμ΄μ–ΈνŠΈ λ²ˆλ“€μ— 값이 ν¬ν•¨λ˜λ―€λ‘œ, μ—¬μ „νžˆ 개발자 도ꡬλ₯Ό 톡해 ν‚€κ°€ λ…ΈμΆœλ  수 μžˆλ‹€λŠ” 점을 인지해야 ν•©λ‹ˆλ‹€. λ”°λΌμ„œ ν΄λΌμ΄μ–ΈνŠΈμ—μ„œ 직접 μ‚¬μš©λ  μˆ˜λ°–μ— μ—†λŠ” 곡개(public) API 킀에 ν•œν•΄μ„œ μ‚¬μš©ν•΄μ•Ό ν•©λ‹ˆλ‹€. (예: Google Analytics ID, 곡개용 지도 API ν‚€ λ“±). μ€‘μš”ν•œ 것은 ν•΄λ‹Ή μ„œλΉ„μŠ€μ—μ„œ μ œκ³΅ν•˜λŠ” **도메인 μ œν•œ(Domain Restrictions)**μ΄λ‚˜ **IP μ œν•œ(IP Restrictions)**κ³Ό 같은 μΆ”κ°€ λ³΄μ•ˆ 섀정을 λ°˜λ“œμ‹œ μ μš©ν•˜μ—¬ μ˜€μš©μ„ λ°©μ§€ν•΄μ•Ό ν•©λ‹ˆλ‹€.

핡심은 "κ³΅κ°œλ˜μ–΄λ„ λ¬΄λ°©ν•œ ν‚€"와 "μ ˆλŒ€ κ³΅κ°œλ˜μ–΄μ„œλŠ” μ•ˆ λ˜λŠ” ν‚€"λ₯Ό κ΅¬λΆ„ν•˜κ³ , ν›„μžμ˜ 경우 λ°˜λ“œμ‹œ μ„œλ²„ μΈ‘μ—μ„œ κ΄€λ¦¬ν•˜λŠ” κ²ƒμž…λ‹ˆλ‹€.

Before Code (Bad)

// src/config.js (λ―Όκ°ν•œ API ν‚€λ₯Ό 직접 포함)
export const GOOGLE_MAPS_API_KEY = "AIzaSy_YOUR_SUPER_SECRET_GOOGLE_MAPS_KEY_XYZ123";
export const STRIPE_SECRET_KEY = "sk_test_YOUR_STRIPE_SECRET_KEY_ABC456"; // μ ˆλŒ€ 곡개되면 μ•ˆ λ˜λŠ” λΉ„λ°€ ν‚€!

// src/App.js
import { GOOGLE_MAPS_API_KEY, STRIPE_SECRET_KEY } from './config';

function App() {
  // ν΄λΌμ΄μ–ΈνŠΈμ—μ„œ λ―Όκ°ν•œ ν‚€λ₯Ό 직접 μ‚¬μš©ν•˜λŠ” μ½”λ“œ (맀우 μœ„ν—˜)
  // Google Maps API Keyλ₯Ό μ‚¬μš©ν•˜μ—¬ 지도 λ‘œλ“œ
  // Stripe 결제 연동 (μ„œλ²„ μΈ‘μ—μ„œ μ²˜λ¦¬ν•΄μ•Ό ν•  secret keyλ₯Ό ν΄λΌμ΄μ–ΈνŠΈμ—μ„œ μ‚¬μš©)
  console.log("Google Maps API Key:", GOOGLE_MAPS_API_KEY);
  console.log("Stripe Secret Key:", STRIPE_SECRET_KEY); // 이 ν‚€λŠ” μ ˆλŒ€ ν΄λΌμ΄μ–ΈνŠΈμ— 있으면 μ•ˆ λ©λ‹ˆλ‹€!

  return (
    <div>
      <p>Your map or payment integration here</p>
    </div>
  );
}

After Code (Good)

// 방법 1: ν™˜κ²½ λ³€μˆ˜λ₯Ό ν†΅ν•œ λΉŒλ“œ μ‹œ μ£Όμž… (곡개 킀에 ν•œν•¨, Vite.js μ˜ˆμ‹œ)
// 1. .env 파일 (ν”„λ‘œμ νŠΈ 루트 디렉토리)
//    VITE_APP_GOOGLE_MAPS_API_KEY="AIzaSy_YOUR_GOOGLE_MAPS_PUBLIC_KEY_XYZ123"
//    (μ°Έκ³ : VITE_ μ ‘λ‘μ‚¬λŠ” Viteκ°€ ν΄λΌμ΄μ–ΈνŠΈ λ²ˆλ“€μ— λ…ΈμΆœν•  ν™˜κ²½ λ³€μˆ˜λ₯Ό μ‹λ³„ν•˜λŠ” λ°©λ²•μž…λ‹ˆλ‹€)

// 2. src/App.js
function App() {
  // 곡개용 API ν‚€ (예: 지도 API, ν΄λΌμ΄μ–ΈνŠΈ μ „μš© 뢄석 툴 ν‚€ λ“±)
  // ν•΄λ‹Ή μ„œλΉ„μŠ€μ—μ„œ μ œκ³΅ν•˜λŠ” 도메인/IP μ œν•œ λ“± λ³΄μ•ˆ 쑰치 ν•„μˆ˜!
  const googleMapsApiKey = import.meta.env.VITE_APP_GOOGLE_MAPS_API_KEY;

  console.log("Google Maps API Key (Public):");

  return (
    <div>
      <p>Using environment variables for public keys.</p>
    </div>
  );
}

// 방법 2: λ°±μ—”λ“œ ν”„λ‘μ‹œ μ„œλ²„λ₯Ό ν†΅ν•œ μ•ˆμ „ν•œ 관리 (ꢌμž₯, λ―Όκ°ν•œ λΉ„λ°€ 킀에 ν•„μˆ˜)
// 1. ν”„λ‘ νŠΈμ—”λ“œ μ½”λ“œ: src/api.js
async function processPayment(paymentDetails) {
  // ν΄λΌμ΄μ–ΈνŠΈλŠ” λ―Όκ°ν•œ Stripe Secret Keyλ₯Ό 직접 μ‚¬μš©ν•˜μ§€ μ•Šκ³ ,
  // λ°±μ—”λ“œ ν”„λ‘μ‹œ μ„œλ²„μ˜ μ—”λ“œν¬μΈνŠΈλ‘œ 결제 μš”μ²­μ„ λ³΄λƒ…λ‹ˆλ‹€.
  const response = await fetch('/api/process-payment', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(paymentDetails),
  });
  if (!response.ok) {
    throw new Error('Payment failed');
  }
  return response.json();
}

// 2. λ°±μ—”λ“œ (Node.js + Express μ˜ˆμ‹œ)
// server.js
// require('dotenv').config(); // .env νŒŒμΌμ—μ„œ ν™˜κ²½ λ³€μˆ˜ λ‘œλ“œ
// const express = require('express');
// const app = express();
// const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); // μ„œλ²„μ—μ„œ ν™˜κ²½ λ³€μˆ˜λ‘œ μ•ˆμ „ν•˜κ²Œ 관리

// app.use(express.json());

// app.post('/api/process-payment', async (req, res) => {
//   const { amount, tokenId } = req.body;
//   try {
//     const charge = await stripe.charges.create({
//       amount,
//       currency: 'usd',
//       source: tokenId,
//       description: 'Example Charge',
//     });
//     res.json({ success: true, charge });
//   } catch (error) {
//     console.error('Stripe error:', error);
//     res.status(500).json({ message: 'Payment processing failed', error: error.message });
//   }
// });

// app.listen(3000, () => console.log('Proxy server listening on port 3000'));