Part 3. 시간 및 상태

58개 중 3개. 동시 또는 거의 동시 수행을 지원하는 병렬 시스템, 하나 이상의 프로세스가 동작하는 환경에서 시간과 상태를 부적절하게 관리하여 발생한다. 관통 질문: 내가 검사한 그것을, 사용하는 순간에도 여전히 같은 것이라고 보장할 수 있는가? 가이드 원문 순서 그대로 진행(교육 순번 = 원문 번호).


1. 경쟁 조건: 검사시점과 사용시점(TOCTOU) CWE-367

신뢰 경계: access() 로 검사한 파일 이름 → 그 사이 변경 가능 → fopen() 이 실제로 여는 대상 ⚠️ 이름으로 검사하고 이름으로 열지 말라. 검사한 그 핸들을 끝까지 써라.

공격 시나리오: 검사(time of check)와 사용(time of use) 사이의 좁은 시간차를 노려 자원을 바꿔치기하면, 프로그램은 검사에 통과한 것과 다른 대상을 신뢰한 채 다뤄 권한 상승·파일 덮어쓰기로 이어진다.

❌ Bad

void file_operation(char* file)
{
    if (!access(file, W_OK))          /* 검사 시점(TOC) */
    {
        f = fopen(file, "w+");        /* 사용 시점(TOU) — 그 사이 바꿔치기 가능 */
        operate(f);
    }
}

✅ Good

void file_operation(char* file_name)
{
    int fd = open(file_name, O_WRONLY|O_CREAT|O_EXCL, S_IRWXU);  /* 생성·검사 원자적 */
    if (fd == -1) { /* 오류 처리 */ }
    if (fchmod(fd, S_IRUSR) == -1) { /* 오류 처리 */ }           /* chmod 대신 fchmod */
    close(fd);
}

치환 규칙: access()+fopen() 이름 기반 처리 금지 → 파일 디스크립터 기반으로 열고 그 핸들을 계속 사용, open(O_CREAT|O_EXCL) 로 생성·검사를 원자화, chmod 대신 fchmod.


2. 제대로 제어되지 않은 재귀 CWE-674

신뢰 경계: 외부 입력이 재귀 깊이를 결정 → 종료 조건·깊이 제한 없이 → 재귀 호출 ⚠️ 재귀에는 “언제 멈추는가(base case)“와 “얼마나 깊어질 수 있는가(깊이 제한)” 두 가지 통제가 모두 필요하다.

공격 시나리오: 공격자가 재귀 깊이를 키우는 입력을 주면 스택 오버플로우와 CPU·메모리 고갈로 프로세스가 강제 종료되어 서비스 거부(DoS)가 발생한다.

❌ Bad

int fac(n)
{
    return n * fac(n-1);      /* 종료 조건이 없어 무한 재귀 -> 스택 고갈 */
}

✅ Good

int i = 0;
int fac(int n)
{
    if (n <= 0) return 1;        /* 귀납 조건(base case) */
    if (i > 10000) exit(1);      /* 깊이 한계 초과 시 종료 */
    else i += 1;
    return n * fac(n-1);
}

치환 규칙: 모든 재귀 함수에 종료 조건 + 깊이 카운터 상한을 동시에 둔다.


3. 심볼릭명이 정확한 대상에 매핑되어 있지 않음 CWE-386

신뢰 경계: 파일 이름/심볼릭 링크 → 시간에 따라 대상이 바뀔 수 있음 → fopen() 이 실제로 여는 대상 ⚠️ 파일 “이름”은 공격자가 언제든 다른 실체로 갈아끼울 수 있는 포인터라고 생각하라.

공격 시나리오: access() 로 검사한 뒤 fopen() 으로 여는 사이에 파일을 심볼릭 링크로 바꿔치기하면 /etc/passwd 같은 민감 파일을 열거나 덮어쓰게 된다.

❌ Bad

if (!access(file, W_OK))
{
    f = fopen(file, "w+");           /* /etc/passwd 링크 가능 */
    operate(f);
}

✅ Good

char* filename;
if (mkstemp(filename))               /* 랜덤 임시 파일 이름 — 바꿔치기 예측 불가 */
{
    FILE* tmp = fopen(filename, "wb+");
    while ((recv(sock, recvbuf, DATA_SIZE, 0) > 0) && amt)
        amt = fwrite(recvbuf, 1, DATA_SIZE, tmp);
}

치환 규칙: 고정 이름 임시 파일 대신 mkstemp(), 디렉터리 재귀 탐색 시 심볼릭 링크를 따라가지 않도록 검사, 검사와 사용을 파일 디스크립터 단위로 묶어 이름 재해석 자체를 없앤다.


Part 3 미니 체크리스트

  • access()/stat() 뒤에 open()/fopen() 이 이어지는 곳이 있는가(이름 재사용)
  • tmpnam()/mktemp()/tempnam() 이 남아 있는가 → mkstemp() 로 교체했는가
  • 재귀 함수마다 종료 조건과 깊이 상한이 모두 있는가
  • SELECT 로 확인 후 UPDATE 하는 패턴이 있는가 → 트랜잭션/조건부 갱신으로 대체했는가

관련 노트