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);치환 규칙: 자원 식별자(포트·파일명)는 화이트리스트 범위 검사 후 사용, 벗어나면 기본값 또는 중단.
보충 — IP 화이트리스트는 왜 "검사 로직"만으론 부족한가
포트처럼 고정된 범위는 코드에 상수로 박아도 되지만, IP 화이트리스트는 운영 중에 계속 바뀐다 — 협력사 IP 추가, 재택 인원 변경, 클라우드 인스턴스 재할당 등. 화이트리스트를 소스에 하드코딩하면 목록이 바뀔 때마다 재배포가 필요하고, 그 배포 지연 동안 검증은 최신 상태를 반영하지 못한다.
- 화이트리스트는 코드가 아니라 설정으로 분리한다 — 파일·DB·중앙 정책 저장소에서 읽고, 재배포 없이 갱신(hot reload)되게 한다.
- 개별 IP 나열보다 CIDR 대역으로 관리하고, 매칭은 문자열 비교가 아니라 네트워크/마스크 연산으로 한다(문자열 비교는
192.168.1.100과192.168.1.1000같은 우회를 놓친다).- 목록을 못 불러오거나 갱신에 실패하면 기본값은 거부(fail-closed) — “확인 안 되면 통과”가 아니라 “확인 안 되면 차단”이 되어야 한다.
- 화이트리스트 변경 자체를 감사 로그로 남겨 누가 언제 무엇을 추가했는지 추적한다.
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)) { /* -> < */ }
case '>': /* -> > */ break;
/* & " 도 동일하게 & " 로 치환 */
}
}치환 규칙: 출력 직전에 < > & " 를 엔티티로 치환. 태그가 필요하면 허용 태그 화이트리스트만 통과.
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문항. 답을 먼저 말로 설명해보고 아래를 펼친다.
snprintf(cmd, sizeof(cmd), "gzip %s", getenv("LOGFILE")); system(cmd);— 문제가 되는 곳과 이유는?- 경로 조작을 막으려고 입력 문자열에서
".."를 전부 치환 삭제했다. 안전한가? - “우리 서버는 폐쇄망이라 SQL 삽입은 걱정 없다”는 말에 어떻게 답할 것인가?
정답 보기
- 운영체제 명령어 삽입(4번·CWE-78).
LOGFILE은 외부 입력이고, 값에;·백틱·파이프가 들어가면gzip뒤로 임의 명령이 붙는다.snprintf의 길이 제한은 버퍼 오버플로우 방어일 뿐 명령어 삽입과는 무관하다. 근본 해결은 셸을 거치지 않는execve계열로 인자를 배열 분리 전달하는 것.- 안전하지 않다(6번·CWE-22). 치환이 한 번만 수행되므로
"....//"를 넣으면 가운데"../"가 제거되고 남은 문자가 다시"../"를 만든다. URL 인코딩(%2e%2e%2f)이나 유니코드 표현으로도 우회된다. 정답은 디코딩 → 절대경로 정규화 → 허용 루트 접두어 비교.- 폐쇄망은 검증의 대체재가 아니다. 입력 출처는 인터넷만이 아니다 — 내부 사용자, 감염된 업무 단말, 연동 시스템, 배치 파일, 설정값이 모두 입력이다. 망 구성은 시간이 지나면 바뀐다. 코드의 안전성이 네트워크 구성에 의존하면 안 된다.
이 계열 미니 체크리스트
- 질의문·명령·경로를 문자열 이어붙이기(
sprintf/strcat/+)로 조립하는 곳이 있는가 -
system()·ldap_search_ext_s()의 질의 구조 부분(베이스 DN·필터·명령어)에 외부 입력이 직접 들어가는가 - 경로 검사를 문자열 치환으로만 하고 있는가(정규화 후 비교가 아닌가)
- 출력 직전 HTML 이스케이프를 빠뜨린 곳이 있는가
- 자원 식별자(포트·파일명·라이브러리 경로)가 화이트리스트를 거치지 않고 그대로 쓰이는가
관련 노트
- Part 1 목록 — 이 Part 전체 지도
- 1-2. 외부제어·우회 계열
- 1-3. 메모리 경계 계열
- 1-4. 정수·타입변환 계열
- 시큐어코딩가이드 목록 — 상위 허브