시리즈: AI와 함께 코딩 배우기

300개 파일을 손상시킨 찾기-바꾸기

깊은 감사 작업이 무엇을 찾아냈고, 그것이 얼마나 빨리 고쳐졌는지.

발견 두 가지, 감사 한 번 스윕이 마땅한 것보다 더 많이 매칭될 때 300개 파일 손상 process_event→ 뒤죽박죽 on_success()→ 뒤죽박죽 has_access→ 뒤죽박죽 단어 경계 범위 지정 없음 빌드는 그린, 파싱도 정상 수정: git 히스토리에서 복원 하드코딩된 시크릿 secret = "●●●●●●●●●●●" 플레이스홀더처럼 보였다 실제 값이 되어버렸다 수정: 교체 + 환경 변수 수정: CI 안에 시크릿 스캔
피해는 컴파일러가 결코 들여다보지 않는 코드의 사람 레이어에 있었다.

에이전트 기반 코드베이스는 빠르게 움직인다 — 그리고 사람 리뷰어라면 한눈에 잡아냈을 방식으로 실패한다. 공개적으로 공유하는 실패 두 건, 그리고 이제 그것들을 완전히 막아내는 가드레일이 여기 있다.

문제를 찾아 나서는 날은 대개 문제를 찾아내는 날이기도 하다. 이번 토요일 나는 코드베이스에 대한 깊은 감사 작업을 예정에 넣었다. 계획은 기술 부채를 정리하는 것이었다. 텅 빈 except 블록, 하드코딩된 상수, 관례의 표류, 빠르게 달리는 어떤 프로젝트에도 쌓이는 종류의 더미들 말이다.

마음에 들지 않는 두 가지를 발견했다. 둘 다 이제 고쳐졌다. 둘 다 공개적으로 알리고 싶은 종류의 발견이다. 에이전트 기반 코드베이스가 어떻게 실패하는지, 그리고 감사를 수행하는 규율이 어떻게 그 실패를 잡아내는지에 대해 실질적인 무언가를 설명해주기 때문이다.

발견 하나: 코드베이스를 먹어치운 찾기-바꾸기

프로젝트 초기에, 에이전트가 독스트링과 주석에 걸쳐 마케팅 스타일의 스윕을 하도록 요청받은 적이 있었다. 의도는 합리적이었다. 몇몇 공개용 문자열에 등장하는 특정 문구를 다시 짓는 것이었다. 실행은 단어 경계에 대해 충분히 세심하지 않았다.

에이전트가 교체하기로 되어 있던 문구는 더 긴 구절의 일부로 “process,” “success,” “access”라는 단어를 포함하는 특정 마케팅 태그라인이었다. 찾기-바꾸기 작업은 매칭되지 않아야 할 곳에서 그 부분 문자열들을 매칭시켰다. 변수 이름. 함수 이름. 테스트 설명. 인라인 주석. 그 세 부분 문자열이 등장하는 모든 곳에, 에이전트의 교체 문자열이 대신 들어갔다.

결과는 미묘하게 뒤죽박죽이 된 식별자와 산문을 가진 300개의 파일이었다. process_event라는 이름의 변수는 토큰 중간에 “Raku Game Engine Milestone”이 박힌 무언가가 됐다. 함수 설명은 헛소리처럼 읽혔다. 테스트 설명은 존재하지 않는 것을 테스트한다고 주장했다. 코드베이스는 컴파일됐다. 망가진 식별자들이 각자의 파일 안에서 일관됐기 때문이다. 하지만 코드베이스의 사람이 읽을 수 있는 레이어는 곳곳에서 미묘하게 훼손됐다.

이런 종류의 실패가 어떻게 일어나는지 구체적으로 말하고 싶다. 다른 팀들도 부딪힐 종류의 에이전트 기반 실패이기 때문이다.

찾기가 너무 넓게 범위 지정됐다. 에이전트는 문구를 찾아 바꾸도록 지시받았다. 그 문구는 마침 흔한 영어 단어의 부분 문자열이었다. 이것을 범위 지정하는 올바른 방법은 단어 경계(정규식으로 \bword\b)로, 명시적인 대소문자 구분과, 명시적인 파일 확장자 허용 목록과, 명시적인 식별자 맥락 거부 목록과 함께다. 에이전트가 받은 지시에는 이런 제약이 하나도 없었다.

에이전트가 그 범위를 표시하지 않았다. 300개 파일은 많은 파일이다. 작은 마케팅 문구 수정을 위해 300개 파일을 건드리는 PR을 착지시킨 에이전트라면 PR을 여는 시점에 그 범위를 표시했어야 했다. 에이전트는 그러지 않았다. PR 제목은 “독스트링 안의 마케팅 문구 업데이트” 비슷한 것이었다. PR 본문은 파일 개수를 우려사항이 아니라 숫자로 나열했다.

내 리뷰 프로세스가 그것을 잡아내지 못했다. PR diff는 모두 같은 수정처럼 보이는 작은 두 줄짜리 변경 300개 파일이었다. diff는 훑어보면 깔끔한 스윕으로 읽힌다. 손상은 에이전트의 치환이 헛소리를 만들어낸 순간의 파일 내용을 실제로 읽어야만 드러난다. 나는 그러지 않았다. 병합했다.

CI는 이름들이 여전히 파싱됐기 때문에 그것을 잡아내지 못했다. 손상된 식별자들은 구문적으로 유효했다. 컴파일러는 여러분의 변수가 마케팅 태그라인처럼 보이는 무언가로 이름 지어져 있는지 신경 쓰지 않는다. 빌드는 그린이었다. 테스트는 여전히 실행됐다. 피해는 기계 레이어가 아니라 코드의 사람 레이어에 있었다.

이번 토요일 어떻게 고쳤나

스크립트다. 그 스크립트는 세 가지 일을 한다.

하나: 정본 식별자 이름을 재도출한다. 잘못된 찾기-바꾸기가 착지하기 전 git 히스토리로부터, 스크립트는 각 식별자가 원래 무엇이라 불렸어야 하는지 재구성한다. 재구성은 기계적이다. 잘못된 PR이 건드린 각 파일에 대해, PR 이전 버전과 PR 이후 버전을 diff하고, 치환된 각 토큰에 대해 PR 이전 이름으로의 복원을 제안한다. 대부분의 파일은 깔끔하게 복원된다. 소수는 손상 위에 정당한 변경이 겹쳐 쌓여 있어 수동 검토가 필요했다.

둘: grep 기반 정합성 검사. 복원 후에도, 잘못된 PR이 착지한 후 작성된 새 코드 중 일부가 손상된 식별자를 참조하고 있었다. 그 참조들은 손상된 이름에 대해 작성되어 있었다. grep 검사는 잘못된 PR이 착지한 후 작성된 코드 안에서 손상 스타일 식별자에 대한 모든 참조를 찾아내고, 각각을 수동 판단을 위해 표시한다. 이 새 코드는 손상된 이름을 사용하려는 의도였는가(드묾), 아니면 당시 존재하던 어떤 이름이든 그냥 사용한 것인가(대부분의 경우)?

셋: 미래를 위한 가드. 이제 에이전트가 수행하는 모든 찾기-바꾸기 작업은 (a) 단어 경계 범위 지정, (b) 대소문자 구분, (c) 파일 확장자 허용 목록, (d) 그것을 넘으면 에이전트가 표시하고 명시적 리뷰를 요청해야 하는 최대 파일 개수 임계값, (e) 전체 교체를 적용하기 전에 에이전트가 보여줘야 하는 무작위 매치 세 개의 샘플을 명시해야 한다. 이 가드는 Copilot Guide 안에 있으며 이제 모든 찾기-바꾸기 작업 틀 잡기의 일부다.

손상은 이제 복구됐다. 복구를 수행한 감사 스크립트는 저장소 안에 있고, 언제든 실행 가능하며, 증거로 diff 출력이 저장되어 있다. 교훈은 Copilot Guide 안에 있다.

발견 둘: 하드코딩된 HMAC 시크릿

깊은 감사 작업은 내가 더 일찍 잡아냈어야 할 무언가도 찾아냈다. 런타임의 라이선싱 레이어는 라이선스 토큰을 검증하기 위해 HMAC-SHA-256을 사용한다. HMAC 시크릿이 소스 파일 안에 하드코딩되어 있었다. 그 소스 파일은 공개 저장소 안에 있었다. 그 시크릿은 실제 프로덕션 검증 경로에서 사용되는 실제 시크릿이었다.

이것은 오늘의 가장 부끄러운 발견이다. 정직하게 말하고 싶다. 빠르게 움직이는 에이전트 기반 코드베이스에서 정확히 일어나는 종류의 일이고, 그것을 어떻게 잡아낼지에 대한 공개 토론이 비공개 토론보다 더 가치 있기 때문이다.

착지까지 걸어온 경로: 라이선싱 레이어의 초기 버전은 그 레이어가 누구에게든 출시되기 전에 교체될 예정인 플레이스홀더 시크릿 값으로 프로토타입되었다. 프로토타입은 명백해 보이는 개발용 플레이스홀더와 함께 PR로 착지했다. 시간이 지나며, 실제 검증 로직이 그 플레이스홀더 위에 추가됐다. 플레이스홀더는 실제처럼 보이는 검증 코드로 감싸이자 더 이상 플레이스홀더처럼 보이지 않게 됐다. 누군가 알아차렸을 때쯤, 그 시크릿은 프로덕션 스타일 흐름에서 사용되고 있었고 그 파일은 공개 저장소 안에 있었다.

오늘 한 일:

  • 시크릿을 교체했다. 유출된 값은 더 이상 프로덕션 값이 아니다. 새 값은 환경 변수 안에 있으며, 실제 시크릿 없이도 개발 작업이 진행되도록 해주면서 그것에 대해 시끄럽게 경고하는 warnings.warn() 폴백이 개발 환경용으로 있다.
  • 소스 파일에서 하드코딩된 값을 제거했다. 교체는 프로덕션 빌드에서 환경 변수가 설정되지 않으면 명확한 에러 메시지를 내는 getenv다.
  • 흔한 패턴(고엔트로피 문자열, base64 모양의 토큰, 키처럼 보이는 무엇이든)과 매칭되는 하드코딩된 시크릿을 스캔하는 CI 검사를 추가했다. 이 검사는 다음 시도가 착지하기 전에 잡아낼 종류의 작은 인프라다.
  • 비슷한 패턴에 대해 코드베이스의 나머지를 감사하는 후속 작업을 등록했다. 그 감사는 별도 주말의 작업이다. 오늘은 즉각적인 발견을 마무리하는 것에 관한 것이었다.

라이선싱 레이어는 여전히 작동한다. 새 경로는 더 안전하다. 유출된 시크릿은 발견 몇 시간 안에 교체됐다.

이것이 무엇으로 일반화되는가

몇 가지 정직한 요점이다.

에이전트 기반 찾기-바꾸기는 명시적인 범위 지정 규칙이 필요하다. 이것은 프로젝트 역사상 지나치게 넓은 스윕이 나를 문 세 번째다. 처음 두 번은 피해가 덜했다. 이번은 영구적인 가드레일을 정당화할 만큼 나빴다. 그 가드레일은 이제 마련되어 있다.

소스 파일 안의 하드코딩된 시크릿은 도구의 실패가 아니라 규율의 실패다. 실제 시크릿이 공개 파일에 착지하도록 놔두는 팀을 구해줄 도구는 없다. “모든 커밋은 하드코딩된 자격 증명에 대해 리뷰된다”는 규율이 진짜 수정이다. CI 스캔이 도움이 된다. 규율이 중요한 것이다.

감사는 리뷰가 놓친 것을 찾아낸다. PR 단위 리뷰가 놓치는 경향이 있는 실패 모드를 특별히 찾으면서 코드베이스에 대해 예정된 감사 작업을 실행하는 규율은 그럴 만한 가치가 있다. 오늘의 감사는 PR 리뷰가 통과시킨 두 가지를 잡아냈다. 미래의 감사는 다른 것들을 잡아낼 것이다. 주기가 핵심이다.

파트너와 빌더가 여기서 얻어야 할 것

파트너십을 위해 엔진을 평가하고 있다면, 팀에게 “에이전트 기반 지나치게 넓은 스윕” 실패 모드를 어떻게 처리하는지 물어보라. 올바른 답은 명시적인 범위 지정 규칙, 대규모 변경에 대한 의무적 표시, 그리고 감사 작업을 포함한다. 잘못된 답은 “그 문제를 본 적이 없다”다.

여러분 스스로 에이전트 기반 워크플로를 운영하고 있고 최근 하드코딩된 시크릿 감사를 하지 않았다면, 하나 해보라. 무언가가 몰래 들어갔을 확률은 0이 아니다. 지금 그것을 찾는 비용은 작다.

이 글을 읽는 보안 전문가라면 제안이 있다면, 나는 진심으로 관심 있다. 내가 방어하려 하는 실패의 부류는 “사람 리뷰어라면 한눈에 잡아냈을 것을, 에이전트 기반 워크플로가 장려하는 일괄 리뷰 패턴에서는 잡아내지 못하는 에이전트의 행동”이다. 제안을 환영한다.

토요일 오후. 코드베이스가 엄격한 검사를 받았다. 발견 두 건, 둘 다 고쳐졌다. 다음 감사는 일정에 있다.

다시 만드는 일로 돌아간다.

감사받도록 만들어진 런타임

RakuAI는 LLM 제작사와 스마트글래스 제조사가 함께 만드는 공간 런타임이다. 감사로 단련되고, 공개된 교훈으로 강화된다. 우리가 파트너급 신뢰를 위해 어떻게 엔지니어링하는지 보라.

← 전체 글