Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4,761 changes: 4,761 additions & 0 deletions coworkers-swagger.json

Large diffs are not rendered by default.

43 changes: 42 additions & 1 deletion next.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,48 @@
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
/* config options here */
async headers() {
return [
{
source: '/(.*)',
headers: [
// HSTS: HTTPS 강제 (배포 환경에서 유효)
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload',
},
// CSP: 허용할 리소스 출처 제한
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
"style-src 'self' 'unsafe-inline'",
Comment on lines +19 to +20

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-critical critical

Content-Security-Policyscript-src'unsafe-inline''unsafe-eval'을 사용하는 것은 XSS(Cross-Site Scripting) 공격에 매우 취약해질 수 있어 보안상 위험합니다.

  • 'unsafe-inline': 인라인 <script> 태그나 onclick 같은 인라인 이벤트 핸들러를 허용합니다.
  • 'unsafe-eval': eval() 함수 사용을 허용하여 임의의 문자열을 코드로 실행할 수 있게 합니다.

Next.js App Router 환경에서는 대부분 이 옵션들 없이 구현이 가능합니다. 만약 특정 라이브러리 때문에 반드시 필요하다면, 해당 내용을 문서화하고 점진적으로 제거할 계획을 세우는 것이 좋습니다. style-src'unsafe-inline'도 가능하면 제거하는 것을 권장합니다.

Suggested change
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
"style-src 'self' 'unsafe-inline'",
"script-src 'self'",
"style-src 'self'",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI 리뷰가 정확하지만 현실적인 문제가 있습니다.

Next.js App Router는 내부적으로 인라인 스크립트를 사용하고, Storybook이나 외부 라이브러리들이 unsafe-eval을 요구하는 경우가 있어서 무턱대고 지우면 앱이 깨질 수 있어. AI가 제안한 대로 'self'만 남기면 로컬에서 바로 에러 날 가능성이 높습니다. 지금 수준으로 두되 "unsafe-inline은 Next.js 내부 동작 때문에 임시 허용한 상태이고, nonce 기반으로 개선할 수 있다는 점은 알고 있다면 되는 내용입니다.

"img-src 'self' blob: data: https:",
"font-src 'self'",
"connect-src 'self' https://fe-project-cowokers.vercel.app",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

connect-srchttps://fe-project-cowokers.vercel.app URL이 하드코딩되어 있습니다. 이렇게 하면 개발, 스테이징, 프로덕션 등 여러 환경을 관리하기 어려워집니다. 환경 변수에서 URL을 불러오도록 수정하는 것이 좋습니다.

Suggested change
"connect-src 'self' https://fe-project-cowokers.vercel.app",
"connect-src 'self' " + process.env.NEXT_PUBLIC_BACKEND_URL,

"frame-ancestors 'none'",
].join('; '),
},
// 클릭재킹 방지
{
key: 'X-Frame-Options',
value: 'DENY',
},
// MIME 스니핑 방지
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
// 레퍼러 정보 제한
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
],
},
];
},
};

export default nextConfig;
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"react-calendar": "^6.0.0",
"react-dom": "19.2.3",
"react-hook-form": "^7.71.1",
"server-only": "^0.0.1",
"zod": "^4.3.5"
},
"devDependencies": {
Expand Down
49 changes: 49 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 32 additions & 0 deletions src/app/api/auth/_lib/cookies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { cookies } from 'next/headers';

// 액세스 토큰: 짧은 만료 (1시간)
const ACCESS_TOKEN_MAX_AGE = 60 * 60;
// 리프레시 토큰: 긴 만료 (7일)
const REFRESH_TOKEN_MAX_AGE = 60 * 60 * 24 * 7;

export async function setAuthCookies(accessToken: string, refreshToken: string) {
const cookieStore = await cookies();

cookieStore.set('accessToken', accessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
maxAge: ACCESS_TOKEN_MAX_AGE,
});

cookieStore.set('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
maxAge: REFRESH_TOKEN_MAX_AGE,
});
}

export async function clearAuthCookies() {
const cookieStore = await cookies();
cookieStore.delete('accessToken');
cookieStore.delete('refreshToken');
}
Comment on lines +1 to +32

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

쿠키 이름인 'accessToken''refreshToken'이 여러 곳에서 매직 스트링으로 사용되고 있습니다. 오타를 방지하고 유지보수성을 높이기 위해 상수로 정의하여 사용하는 것이 좋습니다.

import { cookies } from 'next/headers';

export const ACCESS_TOKEN_NAME = 'accessToken';
export const REFRESH_TOKEN_NAME = 'refreshToken';

// 액세스 토큰: 짧은 만료 (1시간)
const ACCESS_TOKEN_MAX_AGE = 60 * 60;
// 리프레시 토큰: 긴 만료 (7일)
const REFRESH_TOKEN_MAX_AGE = 60 * 60 * 24 * 7;

export async function setAuthCookies(accessToken: string, refreshToken: string) {
  const cookieStore = await cookies();

  cookieStore.set(ACCESS_TOKEN_NAME, accessToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    path: '/',
    maxAge: ACCESS_TOKEN_MAX_AGE,
  });

  cookieStore.set(REFRESH_TOKEN_NAME, refreshToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    path: '/',
    maxAge: REFRESH_TOKEN_MAX_AGE,
  });
}

export async function clearAuthCookies() {
  const cookieStore = await cookies();
  cookieStore.delete(ACCESS_TOKEN_NAME);
  cookieStore.delete(REFRESH_TOKEN_NAME);
}

Loading
Loading