openlang

타입을 붙인 병합 함수가 `__proto__`까지 따라가는 문제

설정 가져오기 기능이 작은 재귀 병합 함수를 써요. 매개변수 타입은 꼼꼼해 보이지만 프로그램이 기대하는 걸 적어 둔 것일 뿐이에요. 루프는 파싱된 파일에 실제로 있는 키를 그대로 돌아요.

type Settings = { theme: "light" | "dark"; fontSize: number };

function merge(target: any, src: any): void {
  for (const key of Object.keys(src)) {
    const v = src[key];
    if (typeof v === "object" && v !== null) {
      target[key] ??= {};
      merge(target[key], v);
    } else {
      target[key] = v;
    }
  }
}

const saved: Partial<Settings> = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, saved);
console.log(({} as any).isAdmin); // true

JSON.parse는 __proto__를 평범한 자기 속성으로 정의해서 Object.keys가 그걸 돌려줘요. 그러면 일반 객체에서 target[key]는 상속된 접근자를 읽어서 Object.prototype을 얻고, ??=는 그게 nullish가 아니니 그냥 두고, 재귀 호출은 모든 객체가 공유하는 프로토타입에 isAdmin을 써요. JSON.parse가 any를 돌려주니 Partial<Settings> 표기는 아무 말 없이 통과했고요.

고민 중인 수정은 두 가지예요. 루프 안에서 위험한 키 이름을 건너뛰거나, 대상의 모든 단계를 Object.create(null)로 만들어서 부딪힐 접근자 자체를 없애거나요. 두 번째가 더 철저해 보이는데, 그러면 아래쪽 코드가 받는 객체마다 toString이 없어요. null 프로토타입 버전을 오래 유지해 본 분 있나요?

이어지는 대화 2

서로의 근거를 따라
  1. 고유 링크

    이름을 건너뛰는 방식은 영영 끝나지 않는 목록이 돼요. 해 봤거든요. __proto__를 건너뛰어도 {"constructor": {"prototype": {"isAdmin": true}}}가 든 파일은 여전히 Object.prototype에 닿아요. 일반 객체의 target.constructor는 Object이고 그 prototype이 바로 공유되는 그거라서요. 이제 목록에 이름이 세 개인데, 거기서 끝날 거라고 믿기지가 않아요.

    화면 쪽에서 보면 애초에 좀 이상해요. 가져오기 대화 상자는 테마랑 글꼴 크기, 설정 두 개만 보여 줘요. 사용자가 파일을 고르면 대화 상자에 "가져왔어요"가 뜨고, 필요한 키는 처음부터 두 개뿐이었어요. 파싱된 객체에서 그 두 개만 읽고, 하나씩 확인하고, 나머지는 버리면 돼요. 화면이 모르는 키를 도는 코드가 아예 없게요.

  2. 고유 링크

    테마와 글꼴 크기는 그렇게 했어요. 알려진 키 두 개만 읽고 나머지는 무시하는 디코더예요. 그런데 파일의 한 부분은 그 모양에 안 맞아요. shortcuts는 사용자가 정한 이름을 키 조합에 연결하는 거라 읽을 키 목록이 없거든요. 그 부분은 이제 Map으로 들어가요.

    const shortcuts = new Map<string, string>(
      Object.entries(raw.shortcuts).filter(
        (e): e is [string, string] => typeof e[1] === "string",
      ),
    );

    __proto__라는 이름의 단축키도 그냥 항목 하나라서 shortcuts.get("__proto__")이 그 값을 돌려주고, 프로토타입은 끼어들지 않아요. 가져오기 경로에서 병합 함수는 빠졌어요.

같은 질문의 다른 갈래

  1. 의존성의 정리를 인용하기 전에 공리부터 본다

    컴파일되는 정리가 곧 증명된 정리라는 보장은 없다. sorry는 어떤 목표든 닫고, Lean은 경고 하나와 함께 파일을 받아들인다. 다른 곳에서 빌드된 의존성이라면 그 경고는 눈앞에 없다. 확인 수단은 #print axiom…

    답글 1lean-4
  2. sleep 없이 Go 채널 인계 테스트하기

    제가 보는 핸드오프 테스트는 대부분 sleep 하나와 희망 섞인 단언으로 끝나요. 이건 그게 없어도 돼요. 요청에는 식별자가 붙어 있어요. 워커는 버퍼 없는 채널에서 요청을 받아 그 식별자를 요청의 결과 슬롯에 쓰고, 같은…

    답글 23go
  3. 컴파일러는 왜 부호 있는 오버플로 검사를 지웠을까?

    부호 있는 int에 대한 if (x + 1 < x)는 오버플로 방어가 아니다. C에서 부호 있는 정수 오버플로는 정의되지 않은 동작이므로, 컴파일러는 x + 1이 절대 넘치지 않는다고 가정하고 조건을 거짓으로 결론 내린 뒤…

    답글 4c