MCP 서버 배포 전 확인할 인증과 HTTP 경계
MCP 서버의 HTTP 전송을 열기 전에 인증, 파일 경로, API URL, Host·Origin 검증을 확인하는 방법을 정리한다.
2026-09-28
서버 자격증명이 누구의 요청을 처리하는지 확인한다
MCP 서버에 Jira API 토큰이나 GitLab PAT를 넣고 HTTP 전송을 열었다면, 첫 점검 대상은 포트 자체보다 그 포트에 도착한 요청을 서버가 누구의 요청으로 취급하는가다. 2026년 9월 공개된 GitHub 보안 권고 네 건에서는 인증이 없는 요청이 서버 자격증명으로 실행되거나, 요청 입력이 서버의 파일과 외부 API 호출 경로를 바꿨다.
| 보안 권고 | 확인할 배포 조건 | 끊어진 경계 | 패치 버전 |
|---|---|---|---|
| mcp-atlassian 인증 우회 | HTTP 전송과 서버 환경변수의 Atlassian 자격증명 | 요청자 인증 → 서버 자격증명 사용 | 0.22.0 |
| mcp-gitlab 파일 읽기 | SSE=true와 upload_markdown |
무인증 툴 호출 → 서버 파일 읽기 | 2.1.27 |
| mcp-gitlab API URL 변경 | ENABLE_DYNAMIC_API_URL=true |
요청 헤더 → 토큰을 싣는 외부 요청의 목적지 | 2.1.27 |
| mcp-gitlab DNS 리바인딩 | 로컬 Streamable HTTP 전송 | 브라우저의 Host·Origin → 로컬 MCP 서버 | 2.1.27 |
GitLab 서버를 운영한다면 먼저 배포 버전이 @zereight/mcp-gitlab 2.1.27인지 확인한다. Atlassian 서버는 mcp-atlassian의 0.22.0 미만이 영향 범위다. 버전 확인 다음에는 실제로 켠 전송 방식과 인증 설정을 대조해야 한다. 같은 서버라도 SSE와 Streamable HTTP에서 드러난 결함이 다르다.
Atlassian 사례에서는 인증 헤더가 없는 HTTP 요청이 툴 핸들러에 도달하고, 핸들러가 서버 환경변수의 자격증명을 사용한다. 아래 흐름에서 점검할 지점은 요청이 툴에 도달하기 전 인증이 강제되는지다.
Atlassian 서버에서는 인증 실패와 자격증명 대체를 함께 본다
영향을 받는 mcp-atlassian에서는 두 동작이 겹친다. OAuth 프록시 인증 제공자가 기본적으로 비활성이고, Authorization 헤더가 없을 때 미들웨어가 요청을 거절하지 않는다. 사용자 토큰이 없는 상태에서 Jira·Confluence 요청은 JIRA_API_TOKEN 같은 서버 환경변수의 자격증명으로 처리된다. 토큰 검증기 역시 비어 있지 않은 문자열을 유효한 토큰으로 받아들인다.
따라서 서버 측 자격증명을 설정한 단일 사용자 배포를 HTTP로 제공한다면, 헤더가 없는 요청과 임의의 Bearer 토큰을 보낸 요청이 모두 거절되는지 확인해야 한다. 보안 권고에 따르면 영향 버전에서는 네트워크로 HTTP 전송에 닿는 요청자가 운영자 권한으로 Jira·Confluence 작업을 할 수 있다. 작업이 서버 자격증명으로 실행되어 감사 로그에도 운영자가 행위자로 남을 수 있다.
이 경계는 Docker 포트 매핑, 인증 없이 연결된 리버스 프록시, 접근 가능한 사내망에서도 확인해야 한다. 권고가 제시한 수정 방향은 인증 제공자가 없는 HTTP 전송의 기동을 명시적 단일 사용자 모드 없이 거부하고, 단일 사용자 모드의 기본 바인딩을 127.0.0.1로 두며, 다중 사용자 배포에는 OAuth 프록시나 실제 토큰 검증을 요구하는 것이다. 배포자는 0.22.0 적용 여부와 함께 자신의 HTTP 경로가 요청을 거절하는지 확인해야 한다.
GitLab SSE에서는 툴 접근과 파일 경로를 따로 점검한다
@zereight/mcp-gitlab의 영향 버전에서 SSE=true이면 /sse와 /messages에 인증 미들웨어가 없다. 권고에 나온 Docker Compose 설정은 3002:3002로 포트를 매핑한다. 이 전송에 네트워크로 닿는 요청자는 서버가 제공하는 툴을 호출할 수 있다.
그중 기본 활성화된 users 툴셋의 upload_markdown은 입력받은 file_path를 제한하지 않고 읽어 GitLab 프로젝트에 업로드했다. 권고에 제시된 핵심 코드에서는 경로 존재 여부를 확인한 다음 파일을 그대로 읽는다.
async function markdownUpload(projectId: string, filePath: string) {
if (!fs.existsSync(filePath)) {
throw new Error(`File not found: ${filePath}`);
}
const fileBuffer = fs.readFileSync(filePath);
// GitLab 프로젝트에 업로드
}
배포 점검에서는 SSE 엔드포인트에 인증이 있는지와 upload_markdown에 전달되는 경로가 제한되는지를 각각 확인한다. 권고의 재현에서는 /proc/self/environ을 읽어 GITLAB_PERSONAL_ACCESS_TOKEN이 포함된 내용을 업로드했다. Dockerfile에 USER 지시자가 없어 프로세스가 root로 실행된다는 점도 읽을 수 있는 파일 범위를 넓힌다.
네 권고의 입력과 결과를 한 장에 놓으면 점검 범위가 분명해진다. 인증, 파일 경로, API 목적지, 브라우저 오리진은 서로 다른 경계다.
동적 API URL을 켰다면 토큰의 목적지를 제한한다
ENABLE_DYNAMIC_API_URL=true인 영향 버전에서는 요청자가 X-GitLab-API-URL 헤더로 GitLab API의 기본 URL을 바꿀 수 있다. Streamable HTTP 처리 경로의 검사는 new URL(dynamicApiUrl)로 URL 형식만 확인한다. 허용할 호스트를 제한하지 않아, 뒤따르는 API 요청의 Private-Token이 헤더에 적힌 호스트로 전달될 수 있다.
const dynamicApiUrl = req.headers["x-gitlab-api-url"]?.trim();
if (ENABLE_DYNAMIC_API_URL && dynamicApiUrl) {
new URL(dynamicApiUrl);
apiUrl = normalizeGitLabApiUrl(dynamicApiUrl);
}
이 설정을 사용하는 배포에서는 헤더를 누가 보낼 수 있는지만 확인해서는 부족하다. 토큰을 첨부하는 외부 요청이 허용된 GitLab 호스트로만 향하는지 확인해야 한다. URL 형식 검사는 그 확인을 대신하지 않는다. 권고에 기록된 수정 버전은 2.1.27이다.
로컬 바인딩이라도 Host와 Origin을 확인한다
별도 권고는 @zereight/mcp-gitlab 2.1.18의 Streamable HTTP 전송을 다룬다. 기본 호스트가 127.0.0.1이어도, 서버가 예상하지 않은 Host와 Origin을 거절하지 않으면 악성 웹 페이지가 DNS 리바인딩을 통해 로컬 MCP 리스너에 요청을 보낼 수 있다. 해당 버전의 전송 생성 코드에는 enableDnsRebindingProtection, allowedHosts, allowedOrigins 설정이 없고, /mcp 앞에서 두 헤더를 검사하는 Express 미들웨어도 없다.
권고의 재현에서 토큰 없는 initialize는 성공했지만 토큰 없는 tools/list는 401로 막혔다. 이는 툴 인증과 HTTP 접근 경계를 구분해야 한다는 뜻이다. 유효한 Private-Token을 실은 같은 오리진 흐름에서는 툴 호출이 진행됐다. 아래 그림은 루프백 주소를 사용하더라도 브라우저 요청이 MCP 초기화 경로에 도달한 관계를 보여 준다.
운영 중인 Streamable HTTP 서버에서는 루프백 바인딩 여부와 별개로 허용할 Host·Origin을 검사하는지 확인한다. 이어서 예상하지 않은 헤더를 가진 요청이 /mcp의 초기화 경로에 도달하기 전에 거절되는지 확인해야 한다. 이 권고에 기록된 패치 버전 역시 2.1.27이다.
배포 전에는 실제 전송 경로에서 거절 여부를 확인한다
설정 파일에서 버전과 환경변수를 확인한 뒤, 배포에 사용하는 HTTP 경로를 기준으로 점검한다. mcp-atlassian은 인증 헤더가 없거나 임의 토큰이 있는 요청이 툴에 도달하는지, @zereight/mcp-gitlab은 SSE 엔드포인트의 인증과 파일 경로 제한을 확인한다. ENABLE_DYNAMIC_API_URL=true라면 허용되지 않은 API 호스트로 Private-Token이 전달되지 않아야 한다. Streamable HTTP는 예상하지 않은 Host·Origin을 MCP 초기화 전에 거절해야 한다.
패치 버전 확인은 출발점이다. 최종 확인 대상은 현재 배포에서 요청자가 서버 자격증명, 서버 파일, 토큰을 싣는 외부 요청을 어디까지 움직일 수 있는가다.