브라우저 확장 보안 감사 — isTrusted · CSV 인젝션 · URL 스킴
웹페이지의 단어나 자막을 번역·요약해 주는 브라우저 확장을 개인적으로 만들어 씁니다. 번역과 요약은 AWS Bedrock(여러 LLM 을 API 로 부르는 AWS 서비스)에 맡깁니다. 특이한 건 서버를 따로 두지 않고 사용자 브라우저에서 Bedrock 을 직접 부른다는 점입니다.
확장은 사용자가 연 페이지 안에 스크립트를 심어서 도니, 그 페이지의 DOM 을 읽고 선택한 단어나 자막을 집어서 Bedrock 에 넘기고 받은 결과를 다시 그 페이지 위에 띄웁니다. Bedrock 을 부를 자격증명도 확장이 들고 있습니다.
서버가 없으니 배포도 운영도 간단해지는 대신 호출 비용은 사용자 계정에서 바로 나갑니다. 여기에 확장이 사용자가 연 웹페이지 안에서 그 페이지와 같이 돈다는 조건이 겹치니 배포 전에 한 번 훑고 갈 이유가 생겼습니다.
확장의 위협 모델 — 삽입된 페이지가 공격자
일반 웹앱은 내가 만든 페이지 안에서 내 코드가 돌지만, 확장은 남의 페이지에 얹혀서 확장이 삽입된 페이지가 곧 잠재적 공격자가 됩니다. 그 페이지는 확장이 심어 놓은 요소를 건드리고, 이벤트를 합성하고, 확장이 읽어 가는 값을 조작할 수 있습니다. 사용자가 어떤 사이트를 열지는 확장이 정하지 못하니 악의적인 페이지 위에서 도는 상황을 기본값으로 놓고 봐야 합니다.
여기에 Bedrock 직접 호출이 붙으면 공격자가 얻을 게 하나 더 생기니, 확장을 속여서 호출을 일으키면 그 비용은 사용자가 냅니다.
페이지가 만든 클릭이 비용을 태움 — isTrusted
확장은 사용자가 단어를 클릭하거나 번역 버튼을 누르면 Bedrock 을 부릅니다. 사용자가 누른 거니 당연히 사용자가 낸 요청이라고 생각했는데, 그 클릭은 페이지가 만들어 낼 수 있습니다.
// 악성 페이지가 이렇게 하면
element.dispatchEvent(new MouseEvent('click', { bubbles: true }))
사람이 안 눌러도 클릭 이벤트가 발생하고, 확장은 그걸 진짜 클릭으로 알고 Bedrock 을 호출하니, 페이지가 이걸 반복하면 사용자 계정으로 비용이 계속 쌓입니다.
브라우저 이벤트에는 isTrusted 플래그가 있어서 사용자의 실제 행동으로 생긴 이벤트는 true 고
코드가 합성한 이벤트는 false 입니다. 모든 사용자 입력 핸들러 앞에 이 검사를 걸었습니다.
function onWordClick(e) {
if (!e.isTrusted) return // 합성 이벤트 차단
// ... 실제 Bedrock 호출
}
페이지가 아무리 클릭을 합성해도 확장이 무시하니 비용은 사람이 실제로 누를 때만 발생합니다.
단어장 CSV 인젝션
확장에는 저장한 단어를 CSV 로 내보내는 기능도 있어서 그쪽도 같이 봤습니다. CSV 인젝션은 셀 값이
=, +, -, @ 로 시작하면 엑셀이나 스프레드시트가 그걸 수식으로 실행하는 공격입니다.
단어장에 =cmd|... 같은 값이 들어 있으면 그 CSV 를 엑셀로 열 때 수식이 실행될 수 있습니다.
사용자가 직접 타이핑한 단어만 들어간다면 덜 위험하겠지만 웹페이지 쪽에서 자동으로 담기는 경로가
있으면 악성 값이 섞여 들어옵니다.
셀 값이 그 문자로 시작하면 앞에 작은따옴표나 공백을 붙여서 수식으로 해석되지 않게 막았습니다.
출처 링크의 URL 스킴
번역 카드에 원문 출처 링크를 넣는데, 그 URL 이 외부에서 들어올 수 있습니다. javascript: 스킴
링크가 들어오면 사용자가 누르는 순간 스크립트가 실행됩니다.
그래서 링크의 스킴을 http 와 https 로만 제한했습니다. 나머지 스킴(javascript:, data:
등)은 아예 링크로 만들지 않습니다.
모델 출력이 들어가는 숫자 가드
영상 요약에서 챕터를 누르면 그 시각으로 이동하는 기능이 있고, 이동할 시각(seek 값)이 모델 출력에서 옵니다. 모델이 무엇을 뱉을지는 보장이 없으니 숫자가 맞는지, 영상 길이 범위 안인지 가드를 걸었습니다. 밖에서 온 값을 그대로 API 에 넘기지 않는다는 원칙은 LLM 출력에도 그대로 걸립니다.
돈이 드는 API를 브라우저에서 직접 부를 때
확장을 감사할 때는 사용자 입력을 받는 지점을 전부 세 가지로 나눠서 봅니다. 그게 진짜
사용자에게서 온 게 맞는지(isTrusted), 값이 안전한 범위인지, 그리고 밖으로 내보낼 때 실행
가능한 형태가 되지는 않는지(CSV 수식, javascript: 스킴)입니다.
그중에서도 확장이 돈이 드는 API 를 직접 부르는 구조라면 합성 이벤트 경로를 제일 먼저 막습니다.
다른 취약점은 데이터가 새거나 스크립트가 도는 문제지만, 이건 악성 페이지를 한 번 열어 두면
사용자가 아무것도 안 눌러도 청구서가 올라가는데, isTrusted 한 줄이 그걸 막습니다.
댓글 0