본문 바로가기
C.W.K.
Stream
Lesson 03 of 05 · published

엄격한 파서

~13 min · identity-handoff, parsing, validation, security

Level 0릴 입문자
0 XP0/39 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"파서는 엄격해. scheme, host, path, 필드 이름, 몇 번 나오나, 정수 범위, 디코드 길이가 전부 검증돼. 모르는 필드나 반복된 필드는 거절되고."

파서가 관문이야

Ashen Reel 에 닿는 모든 딥링크는 먼저 함수 하나를 지나. 그 함수가 국경 검문소고, 그 엄격함이 곧 보안이야. 놀랄 일에 어깨 으쓱하고 넘어가는 관대한 파서는 친근한 팻말 붙은 안 잠긴 문이야. 명시적으로 받으라고 안 배운 건 뭐든 거절하는 엄격한 파서, 그게 이 트랙이 내내 향해온 벽이고.

모든 걸 검증하고, 아무것도 안 믿어

엄격하다는 건, 네가 신경 쓰는 필드만이 아니라 URL 의 모든 부분을 검사한다는 뜻이야.

  • Scheme 은 정확히 cwkashenreel 이어야 해.
  • Host 랑 path 는 정확히 recall 이랑 /open 이어야 하고. route 하나지 계열이 아니야.
  • 필드 이름 은 허용된 집합에서만 나와야 해. 모르는 필드는 어깨 으쓱이 아니라 거절이고.
  • 같은 필드는 최대 한 번만 나와. 반복된 필드는 거절돼서, 아무도 첫 검사 뒤로 두 번째 값을 몰래 못 밀어넣어.
  • 정수 범위. timestamp_ms 는 unsigned integer 로 파싱돼야 해. float 도, 음수도, 단어도 안 돼.
  • 디코드 길이. id 는 상한이 있어서, 메가바이트짜리 'id' 는 못 들어와.
기본은 거절, 허용은 명시적 예외로만. 신뢰 경계는 '이건 계획에 없었는데' 를 자동으로 '아니오' 로 취급해야 해. 파서가 못 알아보는 입력은 전부 공격자가 설계한 입력이야. 그러니 못 알아본 것의 안전한 기본은 거부고.

검문소 전체가 여기 있어. 몇 줄이나 거절인지 봐.

Postel 의 법칙은 신뢰 경계에선 틀렸어. '받는 건 관대하게' 는 협력하는 peer 끼리엔 친근한 프로토콜을 만들지만, 앱이랑 열린 인터넷 사이엔 물렁하고 악용당하기 쉬운 표면을 만들어. 보안 경계에선 받는 걸 보수적으로 해. 파서가 엄격할수록 공격자가 놀 공간이 좁아지거든.

왜 반복 필드가 중요한가

'반복 필드 거절' 규칙은 parameter pollution 을 만나기 전엔 유난스러워 보여. 근데 층마다 중복 키의 어느 값이 이기는지 생각이 다를 수 있어. 네 검증기는 첫 timestamp_ms 를 검사하는데, 저 아래 어떤 reader 는 마지막 걸 쓰지. 공격자는 딱 그 어긋남에 살아. Ashen Reel 은 그 모호함을 통째로 거부해. 필드 하나에 값 하나, 아니면 링크 전체가 거절. 엄격함은 유난이 아니야. 관대한 reader 둘이 서로 다르게 읽었을 틈을 닫는 거지.

Code

국경 검문소. reject-by-default, route 하나, 반복 없음·swift
enum ParseResult { case ok(RecallOpenRequest); case rejected(String) }

func parseRecallOpen(_ url: URL) -> ParseResult {
    guard url.scheme == "cwkashenreel" else { return .rejected("scheme") }
    guard url.host == "recall", url.path == "/open" else { return .rejected("route") }
    guard let comps = URLComponents(url: url, resolvingAgainstBaseURL: false),
          let items = comps.queryItems else { return .rejected("no-query") }

    let allowed: Set = ["video_id", "timestamp_ms"]   // the entire vocabulary
    var seen = Set<String>()
    var videoID: String?
    var ts: UInt64 = 0
    for item in items {
        guard allowed.contains(item.name) else { return .rejected("unknown \(item.name)") }
        guard seen.insert(item.name).inserted else { return .rejected("repeated \(item.name)") }
        switch item.name {
        case "video_id":
            guard let v = item.value, (1...128).contains(v.count) else { return .rejected("id len") }
            videoID = v
        case "timestamp_ms":
            guard let v = item.value, let n = UInt64(v) else { return .rejected("ts not uint") }
            ts = n
        default: break
        }
    }
    guard let id = videoID else { return .rejected("missing video_id") }
    return .ok(RecallOpenRequest(videoID: id, timestampMs: ts))
}

External links

Exercise

구조화된 입력을 받는, 네가 짠 파서나 입력 핸들러를 골라. 쿼리 파라미터, JSON, 명령 같은 거. 인식 못 하는 필드를 어떻게 다루는지 봐. 그냥 무시해, 입력 전체를 거절해? 그다음 중복 키를 거절하는지도 확인하고. 계획에 없던 건 뭐든 기본이 거부고, 반복 키는 에러가 되게 다시 써. 이제 받아주는 입력이 얼마나 줄었어?
Hint
관대한 파서는 이 두 질문이면 거의 다 들통나. '내가 정의 안 한 필드가 오면 어떻게 돼?' 랑 '필드가 두 번 나오면 어떻게 돼?' 답이 '무시됨' 이랑 '마지막이 이김' 이면 공격 표면을 가진 거야. 엄격한 답, 그러니까 '거절' 이랑 '거절' 이 그걸 닫고.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.