1-1. 삽입(Injection) 계열

출처: 「전자정부 SW 개발·운영자를 위한 C 시큐어코딩 가이드」(3판) · Part 1 중 삽입 계열 7개 관통 질문: 외부 문자열이 질의문 · 명령 · 경로의 **“구조”**를 바꿀 수 있는가? 이 계열은 전부 같은 모양이다 — 신뢰 못 할 문자열을 문자열 그대로 이어붙여 구조화된 명령을 만든다. 그래서 방어도 전부 같은 모양이다: 조립하지 말고, 구조와 데이터를 분리하라.


1. SQL 삽입 CWE-89

신뢰 경계: 사용자 입력(id·password) → 검증 없이 → SQL 질의문의 구조 ⚠️ 쿼리는 문자열이 아니라 “구조”다. 외부 입력이 질의의 구조를 바꿀 수 있으면 그것이 곧 SQL 삽입이다.

공격 시나리오: user_id 에 name' OR 'a'='a'-- 를 주면 WHERE 절이 항상 참이 되어 인증을 통과하고 테이블 전체가 조회된다.

❌ Bad

sprintf(query, "SELECT * FROM members"
    " WHERE username='%s' AND password='%s'",
    userId, password);
SQLExecDirect(statmentHandle, query, SQL_NTS);

✅ Good

regcomp(&unsecurePattern,
    "[^[:alnum:]]|select|delete|update|insert|create|alter|drop",
    REG_ICASE | REG_EXTENDED);
userId = makeSecureString(GetParameter(queryStr, USER_ID_PARAM), MAX_USER_ID_LENGTH);
password = makeSecureString(GetParameter(queryStr, PASSWORD_PARAM), MAX_PASSWORD_LENGTH);
/* 실무에서는 위 필터보다 인자화된 질의문(prepared statement)을 우선한다 */

치환 규칙: 문자열 이어붙이기(sprintf+SQLExecDirect) → 인자화된 질의문(parameterized statement). 부득이하면 예약어·특수문자 금지 + 길이 상한 3종 필터.


2. 자원 삽입 CWE-99

신뢰 경계: 환경변수(getenv("rPort")) → 검증 없이 → 소켓 포트 식별자 ⚠️ 포트·파일·핸들 — 자원의 “이름”을 사용자가 정하게 두는 순간 접근제어는 무너진다.

공격 시나리오: 포트 번호를 외부 입력으로 그대로 받으면 공격자가 원하는 대상으로 소켓을 열게 만들 수 있다.

❌ Bad

char* rPort = getenv("rPort");
serv_addr.sin_port = htons(atoi(rPort));

✅ Good

char* rPort = getenv("rPort");
int port = atoi(rPort);
if (port < 3000 || port > 3003)
    port = 3000;                 /* 허용 범위 밖이면 기본값 */
serv_addr.sin_port = htons(port);

치환 규칙: 자원 식별자(포트·파일명)는 화이트리스트 범위 검사 후 사용, 벗어나면 기본값 또는 중단.


3. 크로스사이트 스크립트(XSS) CWE-79

신뢰 경계: 게시글 입력 → 검증 없이 → HTML 출력 스트림 ⚠️ 입력을 저장할 때가 아니라 화면에 그릴 때 이스케이프하라. 블랙리스트보다 허용 태그 화이트리스트가 안전하다.

공격 시나리오: 게시판 제목에 <script>alert(1)</script> 를 저장하면 그 글을 여는 모든 사용자 브라우저에서 공격자 코드가 실행된다.

❌ Bad

inparam[i].f_val = (char *)fmakeword(stdin, '&', &cl);
unescape_url(inparam[i].f_val);
fprintf(fp, "<FONT SIZE=+1><STRONG>%s</STRONG></FONT>", inparam[0].f_val);

✅ Good

static unsigned char *allowed_formatters[] = { "b","i","strong","p","br","a", /* ... */ };
char *spc_escape_html(const char *input) {
    for (c = input; *c; c++) switch (*c) {
        case '<': if (!spc_allow_tag(c+1)) { /* -> &lt; */ }
        case '>': /* -> &gt; */ break;
        /* & " 도 동일하게 &amp; &quot; 로 치환 */
    }
}

치환 규칙: 출력 직전에 < > & " 를 엔티티로 치환. 태그가 필요하면 허용 태그 화이트리스트만 통과.


4. 운영체제 명령어 삽입 CWE-78

신뢰 경계: 표준입력 인자 → 검증 없이 → system() 명령 문자열 ⚠️ system()에 사용자 문자열이 닿는 순간 셸 권한을 넘겨준 것이다. 허용 목록에 있는 값만 실행하라.

공격 시나리오: cat 프로그램에 "Story.txt; ls" 를 넘기면 파일 출력에 이어 ls 가 실행된다.

❌ Bad

fgets(arg, 80, stdin);
commandLength = strlen(cat) + strlen(arg) + 1;
command = (char *) malloc(commandLength);
strncpy(command, cat, commandLength);
strncat(command, argv[1], commandLength - strlen(cat));
system(command);   /* "Story.txt; ls" -> ls 까지 실행 */

✅ Good

fgets(arg, 80, stdin);
if (strpbrk(arg, ";\\\"'."))
{
    exit(1);                     /* 위험 문자가 있으면 수행하지 않는다 */
}
commandLength = strlen(cat) + strlen(arg) + 1;
if (commandLength < 20)          /* 길이 제한도 함께 */
    command = (char *) malloc(commandLength);

치환 규칙: system() 금지 → 후보 명령어 리스트에서만 선택, 부득이하면 ; \ " ' . 검사 + 길이 상한. 근본 해결은 execve 계열로 인자를 배열 분리 전달.


5. LDAP 삽입 CWE-90

신뢰 경계: 환경변수 필터 문자열 → 검증 없이 → LDAP 질의어 ⚠️ LDAP 필터의 * 와 | 는 SQL의 OR과 같다. 필터 문자열을 사용자가 조립하게 두지 마라.

공격 시나리오: 필터 문자열에 이름 대신 *(와일드카드)를 넣으면 한 사람 조회가 디렉터리 전체 조회로 바뀐다.

❌ Bad

char* filter = getenv("filter_string");
rc = ldap_search_ext_s(ld, FIND_DN, LDAP_SCOPE_BASE,
    filter, NULL, 0, NULL, NULL, LDAP_NO_LIMIT, LDAP_NO_LIMIT, &result);

✅ Good

#define FIND_DN ""
char* filter = "(manager=admin)";   /* 고정값 사용 */
rc = ldap_search_ext_s(ld, FIND_DN, LDAP_SCOPE_BASE,
    filter, NULL, 0, NULL, NULL, LDAP_NO_LIMIT, LDAP_NO_LIMIT, &result);

치환 규칙: 필터·DN을 외부 입력으로 조립 금지 → 상수 또는 화이트리스트로 고정. 불가피하면 = + < > # ; \ 를 이스케이프.


6. 디렉터리 경로 조작 CWE-22/23/36

신뢰 경계: 환경변수(reportName) → 검증 없이 → 파일 접근 경로 ⚠️ 경로는 “문자열 조합” 후가 아니라 “정규화(절대경로 변환)” 후에 검사하라. .. 필터링만으로는 인코딩 우회를 못 막는다.

공격 시나리오: reportName 에 ../../../etc/passwd 를 넣으면 지정 디렉터리 밖 시스템 파일이 삭제된다.

❌ Bad

char* rName = getenv("reportName");
char buf[30];
strncpy(buf, "/home/www/tmp/", 30);
strncat(buf, rName, 30);      /* "../../../etc/passwd" 가능 */
unlink(buf);

✅ Good

fileName = DecodeURLEncodedString(fileName);            /* 먼저 디코딩 */
string userHomePath = GetUserHomeDir(userId);
string filePath = ConvertToAbsolutePath(userHomePath + fileName);
if (userHomePath != filePath.substr(0, userHomePath.length()))
    return;                                              /* 다르면 처리 중단 */

치환 규칙: URL 디코딩 → 절대경로 변환(canonicalize) → 허용 홈 경로 접두어 비교. 파일명은 가능하면 화이트리스트 매핑으로 치환.


7. LDAP 처리 CWE-90

신뢰 경계: 파일에서 읽은 manager 값 → 검증 없이 → LDAP 베이스 DN ⚠️ 질의의 “구조”에 해당하는 부분(베이스 DN, 필터)은 절대 외부 입력으로 조립하지 않는다.

공격 시나리오: 입력받은 manager 값을 베이스 DN에 끼워 넣으면 공격자가 임의 루트를 지정해 디렉터리 전체를 조회하거나 인증을 우회한다.

❌ Bad

char base[MAX], manager[MAX-10];
fgets(manager, sizeof(manager), file);
snprintf(base, sizeof(base), "(user=%s)", manager);
rc = ldap_search_ext_s(ld, base, LDAP_SCOPE_BASE, "manager=m1", NULL, 0, NULL, NULL, ...);

✅ Good

ldap_simple_bind_s(ld, username, password);
rc = ldap_search_ext_s(ld, "ou=NewHires",           /* 베이스를 상수로 고정 */
    LDAP_SCOPE_BASE, "manager=m1", NULL, 0, NULL, NULL, ...);

치환 규칙: 5번과 동일 — 베이스 DN·필터는 상수/화이트리스트 고정. (5번은 필터가, 7번은 베이스 DN이 오염 지점이라는 차이만 있다.)


중간 점검 — 스스로 답해보기

실습 대신 PDF가 제시한 3문항. 답을 먼저 말로 설명해보고 아래를 펼친다.

  1. snprintf(cmd, sizeof(cmd), "gzip %s", getenv("LOGFILE")); system(cmd); — 문제가 되는 곳과 이유는?
  2. 경로 조작을 막으려고 입력 문자열에서 ".." 를 전부 치환 삭제했다. 안전한가?
  3. “우리 서버는 폐쇄망이라 SQL 삽입은 걱정 없다”는 말에 어떻게 답할 것인가?

이 계열 미니 체크리스트

  • 질의문·명령·경로를 문자열 이어붙이기(sprintf/strcat/+)로 조립하는 곳이 있는가
  • system() · ldap_search_ext_s() 의 질의 구조 부분(베이스 DN·필터·명령어)에 외부 입력이 직접 들어가는가
  • 경로 검사를 문자열 치환으로만 하고 있는가(정규화 후 비교가 아닌가)
  • 출력 직전 HTML 이스케이프를 빠뜨린 곳이 있는가
  • 자원 식별자(포트·파일명·라이브러리 경로)가 화이트리스트를 거치지 않고 그대로 쓰이는가

관련 노트