curl 소스 코드에서 strncpy를 제거한 데 이어 strcpy도 금지하고, 크기 검사가 복사 코드와 분리되지 않도록 새 문자열 복사 함수를 도입한 이유를 설명한다.
2025년 12월 29일 · Daniel Stenberg · 댓글 10개
얼마 전, 우리가 curl 소스 코드를 훑어보며 결국 모든 strncpy() 호출을 없앴다고 언급한 적이 있습니다.
strncpy()는 이상한 함수이고 API도 형편없습니다. 대상 버퍼를 널 종료(null-terminate)하지 않을 수도 있고, 대상 버퍼를 0으로 패딩 하기도 합니다. 솔직히 말해 대부분의 코드베이스는 이 함수를 완전히 피하는 편이 더 낫습니다. 사용할 때마다 잠재적인 실수 지점이 되기 때문입니다.
그때 strncpy를 멸종시키는 리라이트(rewrite)를 하면서, 우리는 문자열을 제대로 전부 복사하든가 아니면 오류를 반환하도록 반드시 만들었습니다. 문자열을 일부만 복사하는 것이 올바른 선택인 경우는 드물고, 정말 필요하다면 memcpy로 복사한 뒤 널 종료를 명시적으로 처리하면 됩니다. 그래서 strlcpy 같은 것들도 사용할 이유가 없었습니다.

curl에서 시간에 따른 strncpy 밀도
하지만 strcpy는 유효한 사용처가 있고, API도 그다지 나쁘거나 혼란스럽지는 않습니다. strcpy의 가장 큰 문제는 사용할 때 대상 버퍼의 길이도, 소스 문자열의 길이도 지정하지 않는다는 점입니다.
보통 C 프로그램에서 strcpy는 양쪽(소스와 대상)을 완전히 통제할 수 있을 때만 써야 하므로, 이것은 일반적으로 문제가 되지 않습니다.
하지만 보통 과 항상 은 같은 말이 아닙니다. 우리는 모두 인간이고 실수를 합니다. strcpy를 사용한다는 것은 최소 하나, 어쩌면 두 개의 버퍼 크기 체크가 함수 호출 전에 이루어졌다는 의미입니다. 상황이 좋다면요.
그러나 시간이 흐르면서(수십 년 살아남는 코드를 상상해 봅시다) 서로 다른 사고방식과 접근을 가진 많은 작성자들이 코드를 유지보수하고 패치하고 개선하고 다듬다 보면, 그 크기 체크와 함수 호출이 서로 멀어지며 따로 놀게 될 수 있습니다. 둘이 멀어질수록, 그 사이에 어떤 일이 생겨 체크 중 하나가 무효화되거나 strcpy의 전제 조건이 바뀌어 버릴 위험이 커집니다.
크기 체크가 복사 동작 자체에서 분리되지 않도록 하기 위해, 우리는 며칠 전 문자열 복사 대체 함수를 도입했습니다. 이 함수는 인자로 대상 버퍼, 대상 크기, 소스 버퍼, 소스 문자열 길이 를 받고, 복사와 널 종료 문자가 모두 들어갈 수 있을 때만 실제 복사를 수행합니다.
이 덕분에 memcpy()로 대체 구현이 가능해졌습니다. 이제 우리는 strncpy를 이미 금지했던 것처럼, curl 소스 코드에서 strcpy의 사용도 완전히 금지할 수 있게 되었습니다.
이 함수 버전은 필요한 정보가 더 많아서 strcpy보다 약간 더 번거롭고 일이 늘어납니다. 하지만 이 접근법의 장점이 추가적인 고통을 감수할 만큼의 가치가 있고, 전체를 더 잘 감시(오버사이트)하는 데 도움이 될 거라 믿습니다. 앞으로 시간이 지나면 어떻게 될지 보겠죠. 10년 뒤에 다시 돌아와서 어떻게 발전했는지 봅시다!

curl에서 시간에 따른 strcpy 밀도
cvoid curlx_strcopy(char *dest, size_t dsize, const char *src, size_t slen) { DEBUGASSERT(slen < dsize); if(slen < dsize) { memcpy(dest, src, slen); dest[slen] = 0; } else if(dsize) dest[0] = 0; }
이 변경의 추가적인(사소한) 긍정적 부작용은 물론, 이로 인해 AI 챗봇들이 curl 소스 코드에서 strcpy 사용을 찾아내고 “안전하지 않다”고 우기며 보고서를 만들어내는 일을 효과적으로 막을 수 있다는 점입니다(사람들이 아직도 그런 질문을 하는 모양이더군요). 소스 코드에 strcpy가 들어 있으면, 환각에 기반한 취약점 주장(hallucinated vulnerability claims)을 생성하는 꿀단지처럼 작동한다는 건 이미 여러 번 입증됐습니다.
그래도 결국 그들은 다른 무언가를 찾아내서 또 보고서를 꾸며낼 테니, 총합적으로 이득이 있을지는 모르겠습니다. AI 슬롭은 우리가 이길 수 있는 게임이 아닙니다.
태그: cURL and libcurl, Security
이전 글: A curl 2025 review
LouC (2025년 12월 30일 15:58)
표준 라이브러리 함수에는 “더 안전한” 변형들이 너무 많아서, 이런 것들 때문에 지칠 정도입니다.
“라이브러리 레벨” 함수를 겨냥한다면, 최소한 dest != NULL을 우선 확인해야 하고, src != NULL도 확인해 주면 좋겠네요.
Disconnect3d (2025년 12월 30일 16:45)
참고로 strncpy 같은 함수들은 문자열 리터럴과 함께 실행될 때도 문제가 될 수 있습니다. 사람들이 N바이트 리터럴을 복사/비교하려고 하면서 N±1 같은 값을 잘못 넘기는 실수를 계속 반복하더군요… 과거에 이에 대해 조사한 내용을 다음에 정리했습니다: https://github.com/disconnect3d/cstrnfinder
Alejandro Colomar (2025년 12월 31일 00:38)
이게 ISO C11 부록 K(Annex K)의 strcpy_s()와 본질적으로 같은 것 아닌가요?
그렇다면 strcpy_s()의 알려진 문제들도 적용될 텐데요:
Daniel Stenberg (2025년 12월 31일 11:33)
@Alejandro:
이게 ISO C11 부록 K(Annex K)의 strcpy_s()와 본질적으로 같은 것 아닌가요?
잘 모르겠습니다. 그럴 수도 있겠네요. 저는 C 표준을 연구하지 않고, curl은 아직 C89를 사용합니다…
Alejandro Colomar (2025년 12월 31일 14:36)
실제로는 부록 K의 strncpy_s()에 더 가까워 보입니다. 다만 strncpy_s()는 실패 시 오류 코드를 반환하지만, 당신의 것은 그렇지 않네요.
curl에서 C89를 쓰는 건 알지만, 다른 곳에서 어떤 개발이 이루어지고 있는지 아는 것도 좋습니다. 특히 실패한 API에 대해서요. 부록 K API는 버그를 줄이기보다 늘린다는 악명이 있습니다.
이 API의 알려진 문제를 문서화하고 있으니, 주의 깊게 읽어보시길 바랍니다.
Alejandro Colomar (2025년 12월 31일 14:38)
아… 링크를 게시할 수가 없네요. 제 글에서 제거됩니다.
이걸 읽어보세요(이번엔 되길 바랍니다):
www DOT open-std DOT org SLASH jtc1/sc22/wg14/www/docs/n1969.htm
John Douglas Leitch (2025년 12월 31일 08:29)
curlx_strcopy에 대해 몇 가지 생각:
실패를 나타내는 반환값이 있으면 좋겠습니다. 실패 시(가능하면) dest를 널 종료하는 것만으로는 충분하지 않습니다. 크기 체크가 실패한 건지, 아니면 소스가 실제로 빈 문자열인지 구분할 수 없으니까요. 특히 slen == dsize 같은 오프바이원에서 더 혼란스러울 수 있습니다. 호출자 입장에서 왜 빈 문자열이 나오는지 명확하지 않을 수 있죠. 또 slen이 < dsize가 아닌데 dsize == 0인 경우를 감지할 방법이 전혀 없습니다.
Kjell (2025년 12월 31일 10:31)
이건 curlx_memcopy_and_add_trailing_zero() 같은 함수에 더 가깝지 않나요?
즉, 이 함수는 소스를 문자열로 취급하지 않습니다(0 종료 여부를 확인하지 않죠). 대부분의 사용처가 어차피 src_len에 strlen(src)를 넣는 것 같고(거의 함수 내부에서 해도 될 정도로요).
또 이상한 점은 버퍼 크기 조건이 실패해도 그냥 계속 진행한다는 겁니다. 예를 들어 https://github.com/curl/curl/blob/5f5e000278df1029db2ee3f4499b5ce27c1861b2/lib/mime.c#L605 에서는 복사가 성공한다고 가정합니다(성공 여부와 무관하게 len을 3으로 설정하고, 이는 나쁜 패턴일 수 있습니다).
strcpy보다 나쁘진 않고, mime.c에서 큰 문제도 아니긴 하지만요. 그냥 생각이 들었습니다…. 다르게 생각하신다면 이 댓글은 건너뛰셔도 됩니다.
그리고 왜 void가 아니라 다른 값을 반환하지 않나요? 예를 들어 srclen 같은 유용한 값이라도요.
Daniel Stenberg (2025년 12월 31일 11:31)
반환 코드에 대해 묻는 분들께:
이 함수는 strcpy처럼 항상 동작하도록 의도했습니다. 그래서 assert가 있는 것입니다. 어딘가에서 우리가 실수하면, 운영 환경이 아니라 테스트/퍼징에서 그걸 잡아내도록 가정합니다.
요지는 이 함수가 항상 성공하는 곳에서만 쓰인다는 것이므로, 성공 여부를 확인하는 것은 불필요하다는 겁니다.
@Kjell: 호출의 대부분은 strlen()을 호출하긴 하지만, 전부는 아니어서 함수 내부에서 strlen을 하지 않습니다. 그리고 조건부 strlen()을 하는 방식은 과거에 취약한 것으로 드러난 적이 있습니다. 저는 부모 함수들에서 strlen() 호출을 줄이는 쪽에 더 집중하려 합니다.
Willy (2026년 1월 1일 01:28)
우리도 haproxy에서 몇 년 전에 이걸 금지했고, 다음처럼 정의했습니다:
c#undef strcpy __attribute__warning("\n" " * WARNING! strcpy() must never be used, because there is no convenient way\n" " * to use it that is safe. Use memcpy() or strlcpy2() instead!\n") extern char *strcpy(char *__restrict dest, const char *__restrict src);
처음에는 올바르게 시작하더라도, 어느 순간 갑자기 통제 불가능한 것으로 진화해 버릴 수 있습니다. C 문자열 API는 엉망이고, C를 이해하지 못하는 사람들은 C 개발자를 “자기만의 문자열을 재발명한다”고 비판하지만, 이유는 편리하고 안전하면서도 이식성까지 갖춘 것이 없기 때문입니다. 그래서 모두가 자기만의 함수 세트를 다시 만들게 되죠.
편의성은 채택을 장려하고, 문자열 함수를 대체할 때는 그게 사실 가장 먼저 생각해야 할 점입니다. 당신의 경우는 괜찮아 보이네요 😉