# 테넌트 데이터 격리 보안 진단 리포트

**작성일**: 2025-12-12
**대상 시스템**: SAM Multi-Tenant ERP System
**분석 범위**: 프론트엔드 (Next.js 15) + API 프록시 계층

---

## 1. Executive Summary

### 전체 보안 등급: **🟡 Medium Risk (주의 필요)**

이 시스템은 **HttpOnly 쿠키 기반 인증**과 **API 프록시 패턴**을 사용하여 기본적인 보안 구조는 갖추었으나, **테넌트 데이터 격리에 치명적인 취약점**이 존재합니다.

**핵심 문제**:
- ✅ 토큰 보안: HttpOnly 쿠키로 XSS 방어 양호
- ❌ 테넌트 격리: **클라이언트 사이드에서 tenant.id 검증 없음**
- ❌ 데이터 오염: 테넌트 전환 시 **캐시 미정리**로 데이터 혼합 위험
- ⚠️ API 의존성: **PHP 백엔드가 유일한 방어선** (단일 실패 지점)

---

## 2. 테넌트 데이터 흐름 분석

### 2.1 인증 토큰 흐름 (✅ 양호)

```
[로그인 시]
1. 사용자 → Next.js /api/auth/login (user_id, user_pwd)
2. Next.js → PHP /api/v1/login
3. PHP → Next.js (access_token, refresh_token, user, tenant)
4. Next.js: HttpOnly 쿠키 설정 (access_token, refresh_token)
5. Next.js → 클라이언트 (user, tenant 정보만 전달)

[API 호출 시]
1. 클라이언트 → Next.js /api/proxy/* (토큰 없이)
2. Next.js: HttpOnly 쿠키에서 access_token 읽기 ✅
3. Next.js → PHP /api/v1/* (Authorization: Bearer {token})
4. PHP: 토큰 검증 + tenant.id 추출 ✅
5. PHP → Next.js → 클라이언트 (응답)
```

**보안 평가**: ✅ **양호**
- HttpOnly 쿠키로 JavaScript 접근 차단 (XSS 방어)
- SameSite=Lax로 CSRF 방어
- 토큰은 서버 사이드에서만 처리

---

### 2.2 테넌트 ID 흐름 (❌ 취약)

```
[로그인 응답]
PHP → Next.js:
{
  "user": { "id": 1, "user_id": "test" },
  "tenant": { "id": 282, "company_name": "(주)테크컴퍼니" },  // ← 여기서 tenant.id 전달
  "access_token": "...",
  "refresh_token": "..."
}

[문제점]
1. AuthContext: tenant 정보를 localStorage에 저장
   - localStorage.setItem('mes-currentUser', JSON.stringify({ tenant: { id: 282 } }))
   - ❌ 클라이언트가 tenant.id를 임의 조작 가능

2. API 호출 시: tenant.id를 URL에 포함
   - fetch(`/api/tenants/${currentUser.tenant.id}/item-master-config`)
   - ❌ URL의 tenant.id를 조작하면 다른 테넌트 데이터 접근 가능

3. 프론트엔드 검증 없음:
   - Next.js 프록시: tenant.id 검증 없이 그대로 전달
   - PHP 백엔드만 검증 (단일 실패 지점)
```

**보안 평가**: ❌ **Critical**
- **IDOR (Insecure Direct Object Reference) 취약점**
- 클라이언트가 tenant.id를 조작하여 타 테넌트 데이터 접근 시도 가능

---

### 2.3 클라이언트 사이드 캐시 (⚠️ 위험)

#### localStorage 사용 현황

**AuthContext.tsx (Lines 160-188)**:
```typescript
// localStorage에 사용자 정보 저장
useEffect(() => {
  localStorage.setItem('mes-users', JSON.stringify(users));
}, [users]);

useEffect(() => {
  if (currentUser) {
    localStorage.setItem('mes-currentUser', JSON.stringify(currentUser));
    // ❌ tenant.id 변경 시 기존 테넌트 캐시를 정리하지 않음
  }
}, [currentUser]);
```

**문제점**:
- `mes-users`: 여러 테넌트의 사용자 정보가 혼합될 가능성
- `mes-currentUser`: tenant 정보 포함, 조작 가능
- ❌ **테넌트 전환 시 캐시 정리 로직 없음**

#### sessionStorage 사용 현황

**masterDataStore.ts (Lines 82-142)**:
```typescript
const STORAGE_PREFIX = 'page_config_';
// 저장 키: 'page_config_item-master', 'page_config_quotation'

function getConfigFromSessionStorage(pageType: PageType): PageConfig | null {
  const cachedData = window.sessionStorage.getItem(`${STORAGE_PREFIX}${pageType}`);
  // ❌ tenant.id 검증 없음 - 다른 테넌트 데이터가 남아있을 수 있음
  return cachedData ? JSON.parse(cachedData) : null;
}
```

**문제점**:
- ❌ **tenant.id가 캐시 키에 포함되지 않음**
- 사용자 A (tenant 282) → 사용자 B (tenant 300) 전환 시
  - 기존 캐시 `page_config_item-master`가 남아있음
  - 사용자 B가 사용자 A의 설정을 보게 됨

#### TenantAwareCache (✅ 우수한 설계)

**TenantAwareCache.ts (Lines 54-123)**:
```typescript
private getKey(key: string): string {
  return `mes-${this.tenantId}-${key}`;  // ✅ tenant.id 기반 키 생성
}

get<T>(key: string): T | null {
  const parsed: CachedData<T> = JSON.parse(cached);

  // ✅ tenant.id 검증
  if (parsed.tenantId !== this.tenantId) {
    console.warn(`tenantId mismatch: ${parsed.tenantId} !== ${this.tenantId}`);
    this.remove(key);
    return null;
  }

  // ✅ TTL 검증
  if (Date.now() - parsed.timestamp > this.ttl) {
    this.remove(key);
    return null;
  }

  return parsed.data;
}
```

**평가**: ✅ **우수**
- tenant.id 기반 캐시 키 생성
- tenant.id 불일치 시 자동 삭제
- TTL로 만료 처리

**문제**: ⚠️ **masterDataStore가 TenantAwareCache를 사용하지 않음**

---

## 3. 위험 지점 분석

### 3.1 🔴 CRITICAL - API 엔드포인트 테넌트 ID 노출

**파일**: `/src/app/api/tenants/[tenantId]/item-master-config/route.ts`

```typescript
export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ tenantId: string }> }
) {
  const { tenantId } = await params;  // ❌ URL에서 받은 tenantId를 그대로 사용

  const phpEndpoint = `/api/v1/tenants/${tenantId}/item-master-config`;
  return proxyToPhpBackend(request, phpEndpoint, { method: 'GET' });
}
```

**취약점**:
1. **IDOR 공격 가능**: 클라이언트가 URL의 `tenantId`를 조작 가능
   - 정상: `GET /api/tenants/282/item-master-config`
   - 공격: `GET /api/tenants/300/item-master-config` (다른 테넌트)

2. **검증 없음**: Next.js 프록시가 토큰의 tenant.id와 URL의 tenantId 일치 여부를 확인하지 않음

3. **PHP 의존**: PHP 백엔드만 검증 → 백엔드 버그 시 전체 시스템 무방비

**공격 시나리오**:
```javascript
// 공격자가 브라우저 콘솔에서 실행
fetch('/api/tenants/300/item-master-config', {
  method: 'GET',
  headers: { 'Content-Type': 'application/json' }
})
.then(res => res.json())
.then(data => console.log('타 테넌트 데이터:', data));
// ← PHP 백엔드가 방어하지 않으면 데이터 유출
```

**개선 권고**:
```typescript
export async function GET(request: NextRequest, { params }: { params: Promise<{ tenantId: string }> }) {
  const { tenantId } = await params;

  // ✅ 토큰에서 실제 tenant.id 추출
  const token = request.cookies.get('access_token')?.value;
  if (!token) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const decoded = decodeJWT(token);  // JWT에서 tenant.id 추출

  // ✅ URL의 tenantId와 토큰의 tenant.id 일치 여부 확인
  if (decoded.tenant_id !== parseInt(tenantId, 10)) {
    console.warn(`[Security] tenant.id mismatch: token=${decoded.tenant_id}, url=${tenantId}`);
    return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
  }

  // 검증 통과 후 백엔드 호출
  const phpEndpoint = `/api/v1/tenants/${tenantId}/item-master-config`;
  return proxyToPhpBackend(request, phpEndpoint, { method: 'GET' });
}
```

---

### 3.2 🔴 CRITICAL - 클라이언트 사이드 tenant.id 조작 가능

**파일**: `/src/contexts/AuthContext.tsx`

```typescript
export interface User {
  userId: string;
  name: string;
  tenant: Tenant;  // ← tenant.id 포함
}

// localStorage에 저장
useEffect(() => {
  if (currentUser) {
    localStorage.setItem('mes-currentUser', JSON.stringify(currentUser));
    // ❌ localStorage는 JavaScript로 조작 가능
  }
}, [currentUser]);
```

**취약점**:
1. **localStorage 조작**: 브라우저 콘솔에서 tenant.id 변경 가능
   ```javascript
   // 공격자가 콘솔에서 실행
   const user = JSON.parse(localStorage.getItem('mes-currentUser'));
   user.tenant.id = 300;  // 다른 테넌트 ID로 변경
   localStorage.setItem('mes-currentUser', JSON.stringify(user));
   window.location.reload();  // 페이지 새로고침
   // ← 이제 모든 API 호출이 tenant 300으로 전송됨
   ```

2. **검증 부재**: AuthContext가 tenant.id의 진위 여부를 확인하지 않음

**개선 권고**:
```typescript
// ✅ 로그인 시 tenant.id를 HttpOnly 쿠키에 저장
// /api/auth/login에서 추가:
response.headers.append('Set-Cookie',
  `tenant_id=${data.tenant.id}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=7200`
);

// ✅ AuthContext에서 tenant.id 검증
useEffect(() => {
  const verifyTenantId = async () => {
    const response = await fetch('/api/auth/verify-tenant');
    const { tenantId } = await response.json();

    if (currentUser?.tenant?.id !== tenantId) {
      console.error('[Security] tenant.id mismatch detected - logging out');
      logout();
    }
  };

  verifyTenantId();
}, [currentUser]);
```

---

### 3.3 🟡 HIGH - 테넌트 전환 시 캐시 미정리

**파일**: `/src/contexts/AuthContext.tsx`

**현재 로직**:
```typescript
// 테넌트 전환 감지 (Lines 190-201)
useEffect(() => {
  const prevTenantId = previousTenantIdRef.current;
  const currentTenantId = currentUser?.tenant?.id;

  if (prevTenantId && currentTenantId && prevTenantId !== currentTenantId) {
    console.log(`[Auth] Tenant changed: ${prevTenantId} → ${currentTenantId}`);
    clearTenantCache(prevTenantId);  // ✅ 이전 테넌트 캐시 삭제
  }

  previousTenantIdRef.current = currentTenantId || null;
}, [currentUser?.tenant?.id]);

// 캐시 삭제 함수 (Lines 203-225)
const clearTenantCache = (tenantId: number) => {
  const prefix = `mes-${tenantId}-`;  // ✅ tenant.id 기반 키 삭제

  Object.keys(localStorage).forEach(key => {
    if (key.startsWith(prefix)) {
      localStorage.removeItem(key);
    }
  });
}
```

**문제점**:
1. ⚠️ **sessionStorage 미정리**: masterDataStore가 사용하는 `page_config_*` 캐시가 남음
   - `page_config_item-master`, `page_config_quotation` 등
   - tenant.id가 키에 포함되지 않아 이전 테넌트 데이터 혼합 가능

2. ⚠️ **Zustand Store 미초기화**: `itemStore`, `masterDataStore` 메모리 캐시가 남음

**데이터 오염 시나리오**:
```
[사용자 A - tenant 282]
1. 품목기준관리 조회 → sessionStorage에 저장
   - page_config_item-master = { sections: [...] }

[사용자 B - tenant 300으로 전환]
2. clearTenantCache(282) 호출
   - ✅ localStorage: 'mes-282-*' 삭제됨
   - ❌ sessionStorage: 'page_config_*' 남아있음

3. 사용자 B가 품목기준관리 접근
   - masterDataStore.fetchPageConfig('item-master') 호출
   - sessionStorage에서 이전 캐시 반환 (tenant 282 데이터)
   - ❌ 사용자 B가 사용자 A의 설정을 보게 됨
```

**개선 권고**:
```typescript
const clearTenantCache = (tenantId: number) => {
  if (typeof window === 'undefined') return;

  const prefix = `mes-${tenantId}-`;

  // ✅ localStorage 정리
  Object.keys(localStorage).forEach(key => {
    if (key.startsWith(prefix)) {
      localStorage.removeItem(key);
    }
  });

  // ✅ sessionStorage 정리 (추가)
  Object.keys(sessionStorage).forEach(key => {
    // tenant.id 기반 키 삭제
    if (key.startsWith(prefix)) {
      sessionStorage.removeItem(key);
    }
    // tenant.id가 없는 공통 캐시도 삭제
    if (key.startsWith('page_config_')) {
      sessionStorage.removeItem(key);
      console.log(`[Cache] Cleared sessionStorage: ${key}`);
    }
  });

  // ✅ Zustand Store 초기화 (추가)
  const { reset: resetItemStore } = useItemStore.getState();
  const { reset: resetMasterDataStore } = useMasterDataStore.getState();
  resetItemStore();
  resetMasterDataStore();
  console.log('[Cache] Reset Zustand stores');
};
```

---

### 3.4 🟡 HIGH - sessionStorage 캐시에 tenant.id 미포함

**파일**: `/src/stores/masterDataStore.ts`

```typescript
const STORAGE_PREFIX = 'page_config_';  // ❌ tenant.id 없음

function setConfigToSessionStorage(pageType: PageType, config: PageConfig): void {
  window.sessionStorage.setItem(
    `${STORAGE_PREFIX}${pageType}`,  // 예: 'page_config_item-master'
    JSON.stringify(config)
  );
}

function getConfigFromSessionStorage(pageType: PageType): PageConfig | null {
  const cachedData = window.sessionStorage.getItem(`${STORAGE_PREFIX}${pageType}`);
  // ❌ tenant.id 검증 없음
  return cachedData ? JSON.parse(cachedData) : null;
}
```

**취약점**:
- 서로 다른 테넌트의 사용자가 같은 브라우저 세션에서 번갈아 로그인하면
- 이전 테넌트의 캐시를 그대로 사용하게 됨

**개선 권고**:
```typescript
// ✅ TenantAwareCache 패턴 적용
const STORAGE_PREFIX = (tenantId: number) => `mes-${tenantId}-page_config_`;

function setConfigToSessionStorage(
  tenantId: number,
  pageType: PageType,
  config: PageConfig
): void {
  const key = `${STORAGE_PREFIX(tenantId)}${pageType}`;
  const cacheData = {
    tenantId,
    data: config,
    timestamp: Date.now(),
  };
  window.sessionStorage.setItem(key, JSON.stringify(cacheData));
}

function getConfigFromSessionStorage(
  tenantId: number,
  pageType: PageType
): PageConfig | null {
  const key = `${STORAGE_PREFIX(tenantId)}${pageType}`;
  const cached = window.sessionStorage.getItem(key);
  if (!cached) return null;

  const parsed = JSON.parse(cached);

  // ✅ tenant.id 검증
  if (parsed.tenantId !== tenantId) {
    window.sessionStorage.removeItem(key);
    return null;
  }

  // ✅ TTL 검증
  if (Date.now() - parsed.timestamp > 600000) {  // 10분
    window.sessionStorage.removeItem(key);
    return null;
  }

  return parsed.data;
}
```

---

### 3.5 🟡 MEDIUM - 로그아웃 시 캐시 미정리

**파일**: `/src/contexts/AuthContext.tsx`

```typescript
const logout = () => {
  if (currentUser?.tenant?.id) {
    clearTenantCache(currentUser.tenant.id);  // ✅ 캐시 정리
  }
  setCurrentUser(null);
  localStorage.removeItem('mes-currentUser');
  console.log('[Auth] Logged out and cleared tenant cache');
};
```

**문제점**:
- ✅ localStorage 정리는 수행
- ⚠️ sessionStorage 정리 누락 (clearTenantCache가 sessionStorage 미처리)
- ⚠️ Zustand Store 초기화 누락

**개선 권고**: 3.3과 동일

---

### 3.6 🟢 LOW - JWT 디코딩 없이 tenant.id 추출 불가

**파일**: `/src/app/api/proxy/[...path]/route.ts`

```typescript
async function proxyRequest(request: NextRequest, params: { path: string[] }, method: string) {
  // 1. HttpOnly 쿠키에서 토큰 읽기
  let token = request.cookies.get('access_token')?.value;

  // ❌ JWT 디코딩 없이 tenant.id 추출 불가
  // ← 현재는 PHP 백엔드에 의존

  // 2. 백엔드로 프록시 요청
  const backendUrl = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/${params.path.join('/')}`;
  const response = await fetch(backendUrl, {
    headers: {
      'Authorization': `Bearer ${token}`,  // ← tenant.id 검증 없이 전달
    },
  });

  return response;
}
```

**취약점**:
- JWT를 디코딩하지 않아 토큰 내 tenant.id를 확인할 수 없음
- URL의 tenant.id와 토큰의 tenant.id 일치 여부를 검증할 수 없음

**개선 권고**:
```typescript
import jwt from 'jsonwebtoken';

async function proxyRequest(request: NextRequest, params: { path: string[] }, method: string) {
  const token = request.cookies.get('access_token')?.value;
  if (!token) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  // ✅ JWT 디코딩 (서명 검증 없이 페이로드만 읽기)
  const decoded = jwt.decode(token) as { tenant_id: number };

  // ✅ URL에 tenant.id가 포함된 경우 검증
  const urlMatch = params.path.join('/').match(/^tenants\/(\d+)\//);
  if (urlMatch) {
    const urlTenantId = parseInt(urlMatch[1], 10);
    if (decoded.tenant_id !== urlTenantId) {
      console.warn(`[Security] tenant.id mismatch: token=${decoded.tenant_id}, url=${urlTenantId}`);
      return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
    }
  }

  // 검증 통과 후 백엔드 호출
  const backendUrl = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/${params.path.join('/')}`;
  return fetch(backendUrl, {
    headers: { 'Authorization': `Bearer ${token}` },
  });
}
```

---

### 3.7 🟢 LOW - middleware.ts에서 tenant.id 검증 없음

**파일**: `/src/middleware.ts`

```typescript
function checkAuthentication(request: NextRequest): {
  isAuthenticated: boolean;
  authMode: 'sanctum' | 'bearer' | 'api-key' | null;
} {
  const accessToken = request.cookies.get('access_token');
  if (accessToken && accessToken.value) {
    return { isAuthenticated: true, authMode: 'bearer' };  // ← tenant.id 확인 안함
  }
  // ...
}
```

**취약점**:
- middleware는 토큰 존재 여부만 확인
- 토큰이 유효한 tenant.id를 포함하는지 검증하지 않음

**영향**: 🟢 **Low**
- middleware는 인증 여부만 확인하는 역할
- 실제 데이터 접근은 API 계층에서 방어

**개선 권고**: 현재 구조에서는 불필요 (API 계층에서 처리)

---

## 4. 개선 권고사항 우선순위

### 4.1 🔴 즉시 조치 필요 (1주 이내)

#### 1. API 엔드포인트에서 tenant.id 검증 추가
**대상**: `/src/app/api/tenants/[tenantId]/**/route.ts` 전체

**작업**:
```typescript
// 공통 유틸리티 함수 생성: /src/lib/api/tenant-validator.ts
import jwt from 'jsonwebtoken';

export function validateTenantId(
  token: string | undefined,
  urlTenantId: string
): { valid: boolean; error?: string } {
  if (!token) {
    return { valid: false, error: 'Unauthorized' };
  }

  const decoded = jwt.decode(token) as { tenant_id: number };
  if (!decoded || !decoded.tenant_id) {
    return { valid: false, error: 'Invalid token' };
  }

  if (decoded.tenant_id !== parseInt(urlTenantId, 10)) {
    console.warn(`[Security] tenant.id mismatch: token=${decoded.tenant_id}, url=${urlTenantId}`);
    return { valid: false, error: 'Forbidden - tenant.id mismatch' };
  }

  return { valid: true };
}

// 모든 tenant API에 적용
export async function GET(request: NextRequest, { params }: { params: Promise<{ tenantId: string }> }) {
  const { tenantId } = await params;
  const token = request.cookies.get('access_token')?.value;

  const validation = validateTenantId(token, tenantId);
  if (!validation.valid) {
    return NextResponse.json({ error: validation.error }, {
      status: validation.error === 'Unauthorized' ? 401 : 403
    });
  }

  // 검증 통과 후 백엔드 호출
  return proxyToPhpBackend(request, `/api/v1/tenants/${tenantId}/...`);
}
```

**영향**: IDOR 공격 차단

---

#### 2. tenant.id를 HttpOnly 쿠키에 저장
**대상**: `/src/app/api/auth/login/route.ts`

**작업**:
```typescript
export async function POST(request: NextRequest) {
  // ... 기존 로그인 로직 ...

  const data: BackendLoginResponse = await backendResponse.json();

  // ✅ tenant.id를 HttpOnly 쿠키에 저장
  const tenantIdCookie = [
    `tenant_id=${data.tenant.id}`,
    'HttpOnly',
    ...(isProduction ? ['Secure'] : []),
    'SameSite=Lax',
    'Path=/',
    `Max-Age=${data.expires_in || 7200}`,
  ].join('; ');

  response.headers.append('Set-Cookie', accessTokenCookie);
  response.headers.append('Set-Cookie', refreshTokenCookie);
  response.headers.append('Set-Cookie', tenantIdCookie);  // ← 추가

  return response;
}
```

**영향**: 클라이언트 조작 방지

---

#### 3. 테넌트 전환 시 sessionStorage 및 Zustand Store 초기화
**대상**: `/src/contexts/AuthContext.tsx`

**작업**:
```typescript
const clearTenantCache = (tenantId: number) => {
  if (typeof window === 'undefined') return;

  const prefix = `mes-${tenantId}-`;

  // localStorage 정리
  Object.keys(localStorage).forEach(key => {
    if (key.startsWith(prefix)) {
      localStorage.removeItem(key);
    }
  });

  // ✅ sessionStorage 정리 (추가)
  Object.keys(sessionStorage).forEach(key => {
    if (key.startsWith(prefix) || key.startsWith('page_config_')) {
      sessionStorage.removeItem(key);
    }
  });

  // ✅ Zustand Store 초기화 (추가)
  try {
    const { reset: resetItemStore } = useItemStore.getState();
    const { reset: resetMasterDataStore } = useMasterDataStore.getState();
    resetItemStore();
    resetMasterDataStore();
    console.log('[Cache] Reset all stores');
  } catch (error) {
    console.error('[Cache] Store reset failed:', error);
  }
};
```

**영향**: 데이터 오염 방지

---

### 4.2 🟡 단기 조치 (1개월 이내)

#### 4. masterDataStore를 TenantAwareCache 패턴으로 리팩토링
**대상**: `/src/stores/masterDataStore.ts`

**작업**:
1. sessionStorage 키에 tenant.id 포함
   ```typescript
   const STORAGE_PREFIX = (tenantId: number) => `mes-${tenantId}-page_config_`;
   ```

2. 캐시 데이터에 tenant.id 포함
   ```typescript
   interface CachedPageConfig {
     tenantId: number;
     data: PageConfig;
     timestamp: number;
   }
   ```

3. tenant.id 검증 로직 추가 (TenantAwareCache 참고)

**영향**: 캐시 격리 보장

---

#### 5. API 프록시에서 JWT 디코딩 및 tenant.id 검증
**대상**: `/src/app/api/proxy/[...path]/route.ts`

**작업**:
```typescript
async function proxyRequest(request: NextRequest, params: { path: string[] }, method: string) {
  const token = request.cookies.get('access_token')?.value;
  if (!token) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  // ✅ JWT 디코딩
  const decoded = jwt.decode(token) as { tenant_id: number };

  // ✅ URL에 tenant.id가 있으면 검증
  const urlMatch = params.path.join('/').match(/^tenants\/(\d+)\//);
  if (urlMatch) {
    const urlTenantId = parseInt(urlMatch[1], 10);
    if (decoded.tenant_id !== urlTenantId) {
      return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
    }
  }

  // 백엔드 호출
  const backendUrl = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/${params.path.join('/')}`;
  return fetch(backendUrl, {
    headers: { 'Authorization': `Bearer ${token}` },
  });
}
```

**영향**: 심층 방어 (Defense in Depth)

---

#### 6. AuthContext에서 tenant.id 주기적 검증
**대상**: `/src/contexts/AuthContext.tsx`

**작업**:
```typescript
// 5분마다 tenant.id 검증
useEffect(() => {
  const verifyTenantId = async () => {
    try {
      const response = await fetch('/api/auth/verify-tenant');
      if (!response.ok) throw new Error('Verification failed');

      const { tenantId } = await response.json();
      if (currentUser?.tenant?.id !== tenantId) {
        console.error('[Security] tenant.id mismatch - logging out');
        logout();
      }
    } catch (error) {
      console.error('[Security] Tenant verification error:', error);
    }
  };

  const interval = setInterval(verifyTenantId, 5 * 60 * 1000);  // 5분
  return () => clearInterval(interval);
}, [currentUser]);
```

**영향**: 런타임 조작 탐지

---

### 4.3 🟢 장기 개선 (3개월 이내)

#### 7. Backend에서 tenant.id 자동 추출 및 URL에서 제거
**대상**: PHP Backend 및 프론트엔드 전체

**개념**:
- JWT 토큰에서 tenant.id를 추출하여 자동으로 쿼리에 적용
- URL에서 tenant.id를 제거하여 클라이언트 조작 불가능하게 변경

**Before**:
```
GET /api/tenants/282/item-master-config
```

**After**:
```
GET /api/item-master-config
Authorization: Bearer {token_with_tenant_id}
```

**Backend 변경**:
```php
// Laravel Middleware
class TenantScopeMiddleware {
  public function handle($request, Closure $next) {
    $tenantId = auth()->user()->tenant_id;

    // 모든 쿼리에 tenant.id 자동 적용
    \DB::table('items')->where('tenant_id', $tenantId);

    return $next($request);
  }
}
```

**영향**: 근본적인 보안 개선 (Zero Trust 원칙)

---

#### 8. 로그 및 모니터링 강화
**대상**: 전체 시스템

**작업**:
1. **tenant.id 불일치 로그 수집**
   ```typescript
   if (decoded.tenant_id !== urlTenantId) {
     await logSecurityEvent({
       type: 'TENANT_ID_MISMATCH',
       userId: decoded.user_id,
       tokenTenantId: decoded.tenant_id,
       urlTenantId: urlTenantId,
       timestamp: Date.now(),
       ip: request.headers.get('x-forwarded-for'),
     });
   }
   ```

2. **비정상 접근 패턴 탐지**
   - 짧은 시간에 다른 tenant.id로 여러 요청
   - 존재하지 않는 tenant.id 접근 시도

3. **알림 설정**
   - tenant.id 불일치 5회 이상 → 관리자 알림
   - 403 Forbidden 10회 이상 → 계정 일시 정지

**영향**: 공격 조기 탐지 및 대응

---

## 5. 보안 체크리스트

### 5.1 즉시 확인 항목 (개발팀)

- [ ] **PHP 백엔드 tenant.id 검증 확인**
  - [ ] 모든 API 엔드포인트에서 JWT의 tenant.id와 요청 데이터의 tenant.id 일치 여부 확인
  - [ ] Eloquent Model에 `tenant_id` 스코프 적용 확인
  - [ ] 테스트: 다른 tenant.id로 API 호출 시 403 반환 확인

- [ ] **클라이언트 사이드 tenant.id 조작 테스트**
  - [ ] 브라우저 콘솔에서 localStorage의 tenant.id 변경 후 API 호출
  - [ ] 네트워크 탭에서 403 Forbidden 응답 확인
  - [ ] PHP 백엔드 로그에서 tenant.id 불일치 경고 확인

- [ ] **캐시 오염 테스트**
  - [ ] 사용자 A 로그인 → 데이터 조회
  - [ ] 사용자 B (다른 tenant)로 전환
  - [ ] sessionStorage, localStorage, Zustand Store 초기화 확인
  - [ ] 사용자 B가 사용자 A의 데이터를 볼 수 없음을 확인

### 5.2 보안 검증 스크립트

```javascript
// 브라우저 콘솔에서 실행하여 tenant.id 격리 테스트

// 1. 현재 tenant.id 확인
const currentUser = JSON.parse(localStorage.getItem('mes-currentUser'));
console.log('현재 tenant.id:', currentUser?.tenant?.id);

// 2. tenant.id 조작 시도
const fakeUser = { ...currentUser, tenant: { ...currentUser.tenant, id: 999 } };
localStorage.setItem('mes-currentUser', JSON.stringify(fakeUser));
console.log('조작된 tenant.id:', 999);

// 3. API 호출 테스트
fetch('/api/tenants/999/item-master-config')
  .then(res => {
    console.log('응답 상태:', res.status);
    if (res.status === 403) {
      console.log('✅ PASS: 403 Forbidden - tenant.id 검증 정상');
    } else {
      console.error('❌ FAIL: tenant.id 검증 없음 - 보안 위험!');
    }
    return res.json();
  })
  .then(data => console.log('응답 데이터:', data))
  .catch(err => console.error('에러:', err));

// 4. 원래 상태로 복구
localStorage.setItem('mes-currentUser', JSON.stringify(currentUser));
console.log('원래 tenant.id로 복구:', currentUser?.tenant?.id);
```

---

## 6. 결론

### 6.1 핵심 보안 위험 요약

| 위험 | 심각도 | 영향 | 현재 방어 | 권장 조치 |
|------|--------|------|----------|----------|
| URL의 tenant.id 조작 (IDOR) | 🔴 Critical | 타 테넌트 데이터 접근 | PHP 백엔드만 검증 | Next.js 프록시에서 검증 추가 |
| localStorage tenant.id 조작 | 🔴 Critical | API 요청 시 tenant.id 변조 | 없음 | HttpOnly 쿠키로 이동 |
| 테넌트 전환 시 캐시 오염 | 🟡 High | 이전 테넌트 데이터 노출 | 부분적 (localStorage만) | sessionStorage + Zustand 초기화 |
| sessionStorage에 tenant.id 미포함 | 🟡 High | 캐시 격리 실패 | 없음 | TenantAwareCache 패턴 적용 |
| 로그아웃 시 캐시 미정리 | 🟡 Medium | 데이터 잔류 | 부분적 | 전체 캐시 정리 |
| JWT 디코딩 없이 검증 불가 | 🟢 Low | 심층 방어 부족 | PHP 백엔드 | JWT 디코딩 추가 |

### 6.2 보안 강화 로드맵

**Week 1-2 (긴급)**:
1. API 엔드포인트 tenant.id 검증 추가
2. tenant.id를 HttpOnly 쿠키로 이동
3. 캐시 정리 로직 개선

**Month 1 (단기)**:
4. masterDataStore 리팩토링
5. API 프록시 JWT 디코딩
6. AuthContext 검증 로직 추가

**Month 3 (장기)**:
7. Backend tenant.id 자동 추출
8. 로그 및 모니터링 시스템 구축

### 6.3 최종 권고

현재 시스템은 **PHP 백엔드에만 의존**하는 단일 실패 지점 구조입니다. **심층 방어(Defense in Depth)** 원칙에 따라:

1. ✅ **프론트엔드에서 1차 검증** (Next.js 프록시)
2. ✅ **백엔드에서 최종 검증** (PHP API)
3. ✅ **런타임 모니터링** (로그 및 알림)

이 3단계 방어를 구축해야 합니다.

**즉시 조치하지 않으면**:
- 공격자가 tenant.id를 조작하여 타 테넌트 데이터에 접근 가능
- 테넌트 전환 시 데이터 오염으로 사용자 경험 저하
- 규정 위반 (GDPR, ISO 27001) 및 법적 책임 발생 가능

---

**작성자**: Security Engineer Agent
**검토 요청**: 시스템 아키텍트, Backend 팀, DevOps 팀
**다음 조치**: 이슈 트래커에 우선순위별 태스크 등록
