린터(Linter)가 잡아내지 못하는 9가지 보안 안티 패턴 (Security Anti-patterns)
요약
린터(Linter)가 잡아내지 못하는 9가지 보안 안티 패턴 중 일부를 소개하며, 구문론적으로는 완벽하지만 보안상 치명적인 코드 작성 방식을 경고합니다. 동적 코드 실행, 문자열 연결을 통한 SQL 구축, 안전하지 않은 난수 생성 등의 위험성을 다룹니다.
핵심 포인트
- 린터는 코드 스타일은 검사하지만 보안 취약점은 잡아내지 못함
- eval()과 같은 동적 코드 실행은 사용자 입력에 매우 취약함
- SQL 쿼리 작성 시 문자열 연결 대신 매개변수화된 쿼리를 사용해야 함
- 보안 관련 토큰 생성 시에는 일반 PRNG 대신 암호학적 난수 생성기를 사용해야 함
당신의 린터(Linter)는 코드용 맞춤법 검사기와 같습니다. 오타를 잡아내는 데는 뛰어나지만, 거짓말을 잡아내는 데는 형편없습니다.
린터는 세미콜론(semicolon)이 빠졌다고 당신에게 소리치겠지만, 그 위 세 줄에 자리 잡은 SQL 인젝션(SQL injection)에는 눈 하나 깜빡하지 않을 것입니다. 그것은 린터의 버그가 아니라 범위(scope)의 문제입니다. 린터는 '스타일(style)'을 강제합니다. 보안은 완전히 다른 곳에 존재합니다. 즉, '실행되는(runs)' 코드와 '실행되어야 하는(should run)' 코드 사이의 간극에 존재합니다.
이 아홉 가지 패턴은 구문론적으로 완벽합니다. 컴파일도 잘 됩니다. 리뷰도 통과합니다. CI(지속적 통합)도 통과하죠. 그리고 누군가 당신보다 먼저 이것들을 발견했을 때, 당신의 일주일을 기분 좋게 망쳐놓을 것입니다.
Iris Code는 저장할 때마다 이 아홉 가지를 모두 잡아냅니다. 하나씩 살펴보겠습니다.
1. 동적 코드 실행 (Dynamic code execution)
eval()은 프로그래밍계의 "내 맥주 좀 들고 있어 봐(hold my beer)"와 같습니다. 문자열을 받아 코드로 실행하는데, 이는 사용자 제어 쿼리 파라미터(query parameter)가 그 문자열이 되기 전까지는 매우 강력하게 들립니다.
# user_filter는 쿼리 파라미터로부터 전달됩니다
result = eval(f"df[df.{user_filter}]")
일부 린터에는 이에 대한 선택적 규칙이 있습니다. 대부분의 코드베이스는 이를 비활성화해 두었는데, 그 이유는 2014년에 누군가가 "정당한" 사용 사례를 가졌었고 아무도 그 결정을 재검토하지 않았기 때문입니다. 실제로 모든 eval()은 당신의 SSH 키를 요구하는 낯선 사람을 대할 때와 똑같은 주의를 기울여야 합니다.
2. 문자열 연결로 구축된 SQL (SQL built with string concatenation)
지금은 2026년인데 우리는 여전히 사용자 입력을 SQL 문자열에 연결(concatenating)하고 있습니다. Bobby Tables는 벌써 대학을 졸업했을 시간입니다.
const query = "SELECT * FROM users WHERE name = '" + name + "'";
당신의 린터는 유효한 문자열 연결로 인식합니다. 결과물이 아무런 보호 장치 없이 데이터베이스 드라이버(database driver)로 직행하고 있다는 사실을 알지도 못하고, 신경 쓰지도 않습니다.
매개변수화된 쿼리(Parameterized queries)는 존재합니다. 수십 년 동안 존재해 왔습니다. 여기서 위험한 패턴은 SQL이 아니라, 사용자 입력이 스스로를 잘 통제할 것이라고 믿는 것입니다.
3. 안전하지 않은 난수 생성 (Insecure random number generation)
Math.random()은 플레이리스트를 섞는 용도로는 괜찮습니다. 하지만 세션 토큰(session tokens), 비밀번호 재설정 코드(password reset codes), 또는 공격자와 사용자 계정 사이를 가로막는 그 어떤 것을 생성하는 용도로는 절대 사용해서는 안 됩니다.
token := fmt.Sprintf("%d", rand.Intn(999999)) // math/rand는 예측 가능함
범용 의사 난수 생성기(PRNGs)는 예측 불가능하게 만드는 것이 아니라, '빠르게' 작동하도록 설계되었습니다. 그것이 핵심입니다. 만약 난수(randomness)가 접근 권한을 보호한다면, 암호학(crypto) 라이브러리를 사용하세요:
- Go:
crypto/rand - JavaScript:
crypto.getRandomValues() - Python:
secrets
간단한 규칙: 만약 난수 값이 문을 지키고 있다면, 그 문이 패턴 잠금 장치가 달린 문이 아닌지 확인하세요.
4. ReDoS에 취약한 정규 표현식 (Regular expressions)
어떤 정규 표현식(regex) 패턴들은 정교하게 조작된 입력값이 들어와 CPU를 완전히 점유해버리는 백트래킹(backtracking) 소용돌이에 빠지기 전까지는 무해해 보입니다. 단 한 번의 요청. 단 하나의 정규 표현식. 그리고 아주 끔찍한 오후가 시작됩니다.
범인은 중첩된 수량자(nested quantifiers)입니다:
(a+)+
(.*)*
이것들은 지수적인 매칭 경로(exponential matching paths)를 생성합니다. 30자리의 입력값만으로도 평가하는 데 몇 분이 걸릴 수 있습니다. 여러분의 린터(linter)는 정규 표현식의 내부 구조를 분석하지 않습니다. 그저 유효한 문자열 인자로만 인식할 뿐입니다.
Iris가 이를 처리하는 방식
Iris Code는 TypeScript, JavaScript, Go, Python 전반에 걸쳐 중첩 수량자 패턴을 탐지하여, 서버를 마비시키기 전에 미리 잡아냅니다.
5. 하드코딩된 localhost URL
모든 개발자가 한 번쯤은 이렇게 합니다. 개발 중에는
이 플래그는 모든 코드베이스에서 비극적인 기원을 가지고 있습니다. 누군가 로컬 개발 중에 자가 서명 인증서(self-signed cert)를 제대로 작동시키지 못해, 이를 false로 바꾸고 // TODO: fix this라는 주석을 남긴 채 다음 작업으로 넘어갔을 것입니다. 그게 2년 전의 일입니다. 주석은 여전히 그 자리에 있고, 플래그는 여전히 false입니다. 중간자 공격 (Man-in-the-middle attacks)은 여전히 가능합니다.
다른 언어에서의 유사한 잘못들:
- Python의
requests라이브러리에서verify=False사용 - Go 언어에서
InsecureSkipVerify: true사용
만약 HTTPS 요청에서 인증서를 검증하지 않는다면, 그것은 단지 불필요한 단계를 더 거친 HTTP를 사용하는 것과 다를 바 없습니다.
7. 활성화된 상태로 방치된 디버그 모드 (Debug mode)
DEBUG = True
단 네 글자. 무한한 후회.
운영 환경(production)에서 디버그 모드를 켜두는 것은 공격자에게 애플리케이션의 가이드 투어를 제공하는 것과 같습니다. 스택 트레이스 (stack traces), 환경 변수 (environment variables), 파일 경로 (file paths), 설정 세부 정보 (configuration details), 그리고 때로는 에러 페이지에 직접 출력된 실제 자격 증명 (credentials)까지 말입니다.
디버그 모드는 개발 편의를 위한 것입니다. 이를 운영 환경에 배포하는 것은 마치 현관문에 "예비 열쇠"라고 적힌 메모와 함께 집 열쇠를 테이프로 붙여놓는 것과 같습니다.
8. 취약한 해싱 알고리즘 (Weak hashing algorithms)
MD5는 1992년에 발표되었습니다. 2004년에는 이미 깨졌습니다. 지금은 2026년인데도 여전히 운영 코드에서 발견되고 있습니다.
password_hash = hashlib.md5(password.encode()).hexdigest()
현대적인 하드웨어는 솔트(salt)를 사용하지 않은 MD5 해시를 결과를 확인하기 위해 Alt-Tab을 누르는 것보다 더 빠르게 해독합니다. SHA-1도 크게 나을 것은 없습니다. 두 알고리즘 모두에 대한 충돌 공격 (collision attacks)은 실질적이며 잘 문서화되어 있습니다.
올바른 도구를 선택하세요:
- 비밀번호: bcrypt, scrypt, 또는 Argon2
- 무결성 검사 (Integrity checks): SHA-256 또는 그 이상
린터(Linter)는 정당한 라이브러리에 대한 유효한 함수 호출을 확인합니다. 그 라이브러리가 박물관에나 있어야 할 유물인지에 대해서는 의견을 내지 않습니다.
9. 오픈 리다이렉트 (Open redirects)
const target = req.query.redirect;
res.redirect(target);
축하합니다, 당신의 신뢰할 수 있는 도메인이 이제 피싱 발사대(phishing launchpad)가 되었습니다. 공격자는 당신의 합법적인 URL로 시작하여 자신이 원하는 곳에서 끝나는 링크를 정교하게 만듭니다. 피해자들은 링크에서 당신의 도메인을 보고 아무 생각 없이 클릭하게 됩니다.
리다이렉트(redirect) 목적지는 항상 허용 목록(allowlist)을 통해 검증하십시오. 목적지가 목록에 없다면 리다이렉트를 수행해서는 안 됩니다.
린터(Linter)가 이를 놓치는 이유
이 예시들 중 그 어떤 것도 구문 오류(syntax error)가 아닙니다. 모든 코드는 유효하며 관용적인(idiomatic) 코드입니다. 위험은 코드가 어떻게 작성되었느냐가 아니라, 코드가 _무엇을 하느냐_에 있습니다:
- 이 입력값은 어디에서 왔는가?
- 이 값은 어디로 향하는가?
- 이것은 무엇을 보호하고 있는가?
- 이 API가 의도된 방식대로 사용되고 있는가?
린터는 구문 패턴(syntax patterns)을 일치시킵니다. 린터는 신뢰 경계(trust boundaries)를 모델링하거나, 데이터 흐름(data flow)을 추적하거나, 문맥(context)을 이해하지 못합니다. 보안에 특화된 일부 린트 규칙(lint rules)이 존재하지만, 성숙한 코드베이스에서 이를 활성화하면 하룻밤 사이에 수백 개의 탐지 결과가 쏟아지기 때문에 팀들은 이를 비활성화하고 그냥 넘어갑니다.
보안 탐지는 다른 접근 방식이 필요합니다: 초기 단계에서, 로컬에서, 그리고 당신이 활발하게 수정 중인 코드 범위 내에서 이루어져야 합니다.
탐지는 이야기의 절반일 뿐입니다
Iris Code는 모든 저장(save) 시점에, 분석을 완전히 로컬 머신에서 실행하여 에디터 내에서 이 9가지 패턴을 모두 표시합니다. 데이터는 외부로 나가지 않습니다. 클라우드 왕복(cloud round-trips)도 없습니다.
하지만 탐지만으로는 한계가 있습니다. 누군가는 경고를 보고도 어깨를 으쓱하며 그냥 머지(merge)해 버릴 수 있기 때문입니다. 이것이 Iris Code가 두 가지 레이어를 더 제공하는 이유입니다:
터미널 스캐닝(Terminal scanning). iris security는 명령줄에서 프로젝트 또는 스테이징된 파일(staged files)을 스캔합니다. 필요할 때마다, 스크립트 내에서, 원하는 곳 어디에서든 실행할 수 있습니다.
강제 가능한 품질 게이트(Enforceable quality gates). .irisconfig.json에서 gateMaxSecuritySmells를 0으로 설정하면 보안 스멜(security smells)이 강력한 중단 지점(hard stop)이 됩니다. Pre-push 훅(hooks)이 실패하고, CI 빌드가 실패합니다. 예외는 없으며, "내 컴퓨터에서는 통과했는데요"라는 변명도 통하지 않습니다.
| 레이어 | 도구 | 실행 시점 |
|---|---|---|
| 에디터 | Iris Code 사이드바 | 저장할 때마다 |
| ... |
설정 파일을 한 번 커밋하십시오. 그 시점부터 팀의 모든 개발자는 동일한 표준에 따라 작업하게 됩니다.
원문은 Iris Code 블로그에 게시되었습니다.
Iris Code는 저장할 때마다 이 9가지 안티 패턴 (Anti-patterns)을 모두 감지하여 에디터 내에서 완전히 오프라인 상태로 표시해 줍니다. 무료로 체험해 보세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기