표준, 그 확산, 그리고 구현 간 미묘한 차이가 어떻게 상호운용성과 보안 문제로 이어질 수 있는지에 대한 글입니다.
표준, 그 확산, 그리고 발생할 수 있는 문제들에 대한 블로그 글입니다. 제가 처음 표준에 관여한 것은 그저 독자로서였습니다. 복잡한 코드나 프로토콜의 예상치 못한 동작을 더 잘 이해하기 위해서였습니다. 시간이 지나면서 저도 관여하게 되었고, 극단적인 경우 구현들이 같은 동작에 맞춰지도록 몇몇 사항을 명확히 하는 일을 도왔습니다. 결국 저는 사양 하나를 공동 편집하게 되었습니다 - Subresource Integrity (SRI)로, 2015년에 W3C 권고안으로 출판되었습니다. SRI의 핵심 아이디어는 예상 파일의 SHA2 다이제스트와 함께 제삼자 JavaScript를 포함하는 것입니다. 브라우저가 다운로드한 URL이 예상한 다이제스트와 일치하지 않는다고 판단하면, 스크립트는 실행되지 않습니다. 이를 통해 JavaScript에 빠른 CDN을 사용하면서도 그들에게 페이지의 스크립트에 대한 완전한 통제권을 주지 않을 수 있으며, 본질적으로 보안 위험을 줄일 수 있습니다.
이러한 다이제스트의 표준 형식은 예를 들어 sha(size)-(base64 encoding of the digest)입니다. 해시 다이제스트를 계산하는 것은 비교적 단순하지만, base64에는 두 가지 인코딩 알파벳이 있습니다. 첫째는 a-zA-Z0-9/+, 둘째는 a-zA-z0-9_-를 사용하는 URL 안전 변형입니다. 사양의 예시는 모두 전자를 사용했습니다.
출판 후 거의 10년이 지난 2025년에야 우리는 여전히 버그 하나를 발견했습니다. Firefox가 어떤 웹사이트를 제대로 지원하지 못한다는 호환성 보고서의 일부로 조사하던 중, 핵심 문제는 사실 다른 브라우저에 있다는 것을 알게 되었습니다. 그 다른 브라우저는 두 종류의 인코딩을 모두 관대하게 받아들였고, 그 결과 웹사이트들은 base64와 base64url을 서로 바꿔 써도 지원될 것이라고 기대하게 되었습니다. 그 페이지는 Firefox에서는 동작하지 않았는데, 웹사이트가 브라우저에게 검사하도록 요구한 모든 해시를 Firefox가 받아들이지 않았기 때문이며, 이는 사소한 보안 문제를 드러냈습니다.
진짜 해결책은 표준이 base64url 변형은 올바르지 않다고 명확히 하고, 다른 브라우저 엔진이 그에 맞게 동작을 바꾸는 것이었을 것입니다.
하지만 표준의 확산, 웹 호환성, 그리고 특정 브라우저들의 불행한 시장 지배력과 관련된 몇 가지 문제 때문에, 우리는 다른 길을 택했습니다. 기존 웹 콘텐츠를 지원하기 위해, 두 종류의 인코딩을 모두 유효한 표현으로 간주한다고 인정하도록 표준을 변경했습니다.
이 예시는 미묘한 차이가 드러나기까지 여러 해가 걸릴 수 있음을 보여줍니다. 상호운용 가능한 사양은 일종의 "행복 경로"를 따라 공유된 이해를 확립할 수는 있지만, 반드시 적대적인 환경에서까지 그런 것은 아닙니다. 또한 표준에는 지속적인 유지보수와, 구현들이 시간이 지나도 상호운용 가능하고 안전하게 유지되도록 보장하는 적극적인 이해관계자들이 필요합니다.
원래 사양은 처음에는 단지 문서화된 설명, 즉 무언가를 더 낫게 만들 수 있는 방법에 대한 아이디어일 뿐입니다. 어떻게 동작해야 하는지, 어떻게 작동하는지, 데이터 구조와 알고리즘, 그리고 그것들 사이의 상호작용이 어떤 모습인지에 대한 설명입니다. 누구나 문법, 파서, 그리고 그 결과 데이터 구조를 생각해낼 수 있습니다.
표준이 되려면, 이 사양에는 널리 그리고 일관되게 구현되는 공유된 합의가 필요합니다. 이는 사양과 구현을 반복적으로 공동 설계하고, 구석진 경우들에 대해 치열하게 논의할 때 가장 잘 작동합니다. 어떤 이들은 더 나아가 공유 테스트 스위트를 사용하기도 합니다.
이렇게 하면 Interoperability (interop)로 이어지지만, 여전히 개별 구현을 넘어선 생태계에 대한 지속적인 유지보수와 관찰이 필요합니다. interop은 점근적이며 시간에 걸친 공유된 합의를 요구하는 반면, 보안은 이해를 요구합니다 - 이는 제한과 미묘한 경계를 살펴봐야 하는 더 넓은 범위입니다.
구현이 사양을 읽지 않은 채 문법이 "충분히 단순하다"고 여길 때, 이러한 더 깊은 수준의 이해는 종종 빠져 있습니다. SRI의 base64 예시는 그저 하나의 사례일 뿐이며, 더 많은 예가 있습니다.
많은 사람들이 텍스트 기반 언어를 위해 자신만의 파서를 작성해 왔습니다. 정규 표현식으로 HTML을 파싱하는 코드를 본 적이 있을지도 모릅니다. "쉽게" 파싱할 수 있는 언어의 또 다른 좋은 예로는 XML, JSON, 또는 YAML이 있을 것입니다.
하지만 이러한 구현들은 종종 서로 다른 가정을 하며, 그 결과 미묘한 비호환성이나 심지어 보안 결함으로 이어지곤 합니다.
좀 더 실용적인 예로, 겉보기에는 단순한 입력을 다루는 일이 어떤 영향을 미치는지 보여주기 위해 JSON의 한 문제를 살펴보겠습니다. 다음 JSON 문자열과 그 결과 데이터 구조를 보겠습니다.
{
"test": 0,
"test": 1
}
이를 객체 obj로 파싱했을 때, obj.test는 무엇을 반환할까요? 대부분의 JSON 파서는 매우 관대해서 같은 이름의 딕셔너리 키 "test" 두 개를 기꺼이 받아들입니다. 어떤 구현은 단순히 obj.test에 두 번 할당할 수 있습니다. 먼저 0을 넣고, 그다음 1로 덮어쓰는 식입니다. 다른 구현은 기존 키가 있는지 확인하고 두 번째 "test" 키를 조용히 거부하여 첫 번째 값을 유지할 수도 있습니다.
JSON을 "JavaScript의 부분집합"으로 설명했던 원래 서술의 엄밀성 부족은 JSON RFC에서 이미 인정되었고 문제로 제기되었습니다. 그 RFC는 훨씬 뒤인 2017년에 나왔습니다. 하지만 오늘날까지도 많은 구현은 중복된 딕셔너리 키가 있는 입력을 허용하고, 서로 다른 동작을 보입니다.
SRI와 JSON의 예시는 비교적 무해하지만, 실제 파서 차이점 버그는 코드 실행, 인증 우회 등으로 이어져 왔습니다1.
완벽한 상호운용성은 사양만으로 만들어지지 않으며, 끊임없는 유지보수가 필요합니다. 모호성은 장기적인 헌신과 구현 및 사용자로부터의 정기적인 피드백을 통해서만 제거될 수 있습니다.
보안도 마찬가지입니다. SRI 버그는 10년 동안 지속되었고, 구현들이 어떻게 의견이 달랐는지, 그리고 구석진 경우들이 어떻게 간과되었는지를 아무도 알아차리지 못했습니다. 그것들은 실제 사용자에게 드러나는 문제가 생기고 나서야 비로소 정렬되었습니다.
하지만 이런 예시들은 경고 신호가 아닙니다. 그것들은 인터넷이 어떻게 만들어지는지를 보여주는 흉터 조직입니다. 표준은 경계심 있는 유지보수를 통해서만 성숙할 수 있습니다.
버그 보고서, 제기되는 사양 이슈, 공유 테스트 케이스, 때로는 무작위 포럼의 불만까지. 이 모든 것은 모호성을 제거하고 인터넷 표준이 성숙하도록 돕습니다.
결국 표준이 안전한 이유는 그것이 문서로 적혀 있기 때문이 아닙니다. 사람들이 계속해서 그것에 의문을 제기하고, 이해하고, 유지보수하기 때문입니다.