본문으로 건너뛰기
SheetForge

웹 플러그인 마켓

웹 앱에는 브라우저에 SheetForge 플러그인을 설치하기 위한 플러그인 마켓이 있으며, 이와는 별도로 GitHub URL로부터 미검수 플러그인을 사이드로딩하는 경로도 있다. UIToolkit으로 다시 만들어진 동일한 마켓이 Unity 안에도 함께 제공된다.

레지스트리 — 정적 파일 하나, 읽는 이는 둘

마켓 목록은 웹 앱과 Unity 내 마켓 창 양쪽이 함께 읽는 하나의 정적 레지스트리 파일(public/registry/plugins.json)에서 온다. 목록이 하나뿐이므로, 두 표면은 항상 같은 카탈로그를 보여준다.

SheetForge는 플러그인 바이너리를 직접 호스팅하지 않는다. 이를 담당하는 것은 각 제작자 자신의 GitHub release다. 레지스트리 항목은 메타데이터와 고정된 해시일 뿐 — 플러그인의 사본이 아니다.

해시 고정 — 코어가 바이트 하나라도 보기 전의 무결성

모든 레지스트리 항목은 그 플러그인이 승인되었을 때 담긴 SHA-256을 갖고 있다. 획득 시점에, 받은 모든 바이트는 코어에 닿기 전에 그 해시와 대조 검증된다. 단 한 바이트라도 일치하지 않으면 거부된다.

이것이 "바이너리가 다른 누군가의 GitHub에 산다"를 안전하게 만드는 방법이다: release는 어디서든 다시 내려받을 수 있지만, 오직 승인된 그 바이트만이 로드된다.

서버 프록시를 통한 원클릭 설치

브라우저는 GitHub release 에셋을 직접 fetch할 수 없다 — 크로스 오리진(CORS) 규칙이 이를 막는다. 그래서 획득은 서버 프록시 경로(api/market/artifact, lib/market/install.tsArtifactProxy를 통해 연결됨)를 거치며, 이는 서버 측에서 release를 fetch해 다시 스트리밍한다. 그래서 설치는 클릭 한 번이다.

프록시가 release에 닿을 수 없을 때를 위한 수동 다운로드-업로드 경로가 보조 대안으로 남아 있다. 이는 동일한 해시 검사를 통과한다 — 업로드된 파일도 프록시를 거친 파일보다 더 신뢰받지 않는다.

호환성 게이트 — 무결성 다음, 등록 이전

해시 검사를 통과하는 것은 그 바이트가 승인된 것임을 증명할 뿐이다. 이 호스트가 그 플러그인의 형식을 읽을 수 있는지는 말해주지 않는다 — 이는 별개의 두 번째 질문이다.

답은 검증된 DLL 자신에서 나온다: 그 플러그인이 어느 세대의 플러그인 형식을 대상으로 빌드되었는지, 그리고 원하는 최저 호스트 버전을 명명하는 어셈블리 수준 선언이다.

  • 목록은 광고할 뿐, 판정하는 것은 어셈블리다. 레지스트리 항목은 동일한 두 값(pluginFormat, minHost)을 담고 있으므로 카탈로그는 다운로드 전에 이를 보여줄 수 있다. 게이트 자신은 방금 검증을 통과한 바이트에서 그 선언을 직접 읽는다. 목록은 오래될 수 있어도, 컴파일된 선언은 그럴 수 없다.
  • 하나의 게이트, 세 개의 경로. 마켓 설치, 사이드로드, 평범한 로컬 파일 모두 동일한 조건을 묻는다 — 그래서 어느 경로도 다른 경로가 거부하는 것을 조용히 받아들일 수 없다.
  • 어셈블리 단위로 전부 아니면 전무다. 거부된 어셈블리는 절반의 계약이 아니라 아무것도 등록하지 않으며, 그 거부는 그 어셈블리가 무엇을 선언했고 이 호스트가 무엇을 읽는지 명명한다.
  • 선언이 없어도 괜찮다. 이 애트리뷰트가 존재하기 전에 만들어진 플러그인은 호스트 요구 사항이 없는 가장 이른 세대로 읽히므로, 항상 그래왔던 것처럼 로드된다.

캐싱과 언로딩

해시 검사를 통과한 바이트는 브라우저에(IndexedDB) 캐시되므로, 목록에서 활성화한 플러그인은 이후 방문마다 자동으로 로드된다 — 한 번만 활성화하면 된다.

플러그인을 비활성화하거나 제거하면 즉시 효력을 발휘한다: 앱이 코어에 그 어셈블리를 플러그인 조립에서 빼고 세션을 다시 세우라고 지시하므로, 그 플러그인의 셀 타입·enum·템플릿·색 프리셋·스튜디오 액션·캔버스 덧칠이 새로고침 없이 사라진다. (.NET 자체는 어셈블리를 언로드하지 않는다 — 바이트는 페이지에 그대로 남되 아무 일도 하지 않는다. 같은 플러그인을 다시 활성화하면 코어는 두 번째로 로드하는 대신 남아 있던 어셈블리를 다시 켠다.)

GitHub URL로 미검수 플러그인 사이드로딩하기

엄선된 레지스트리와는 별개로, 임의의 GitHub URL을 앱에 건네주어 미검수 플러그인을 사이드로드할 수 있다(lib/market/sideload.ts, api/market/github가 이를 처리한다). 이는 아직 카탈로그에 없는 플러그인을 시험해보기 위한 것이다.

이 축은 검증된 레지스트리 항목이 아니라 사용자가 제공한 URL을 받으므로, 여기가 SSRF 방어를 직접 강제하는 유일한 곳이다:

  • URL은 GitHub 계열 호스트github.com, raw.githubusercontent.com, release-assets.githubusercontent.com 등 — 로, 정확히 일치하는지 소문자로 비교해 확인한다.
  • 내부 네트워크로 새어 나갈 수 있는 리다이렉트는 그 경계에서 차단된다.

사이드로드된 아티팩트는 별개의 소유자 태그(sideload:) 아래에 캐시된다. 그래서 우연히 레지스트리 항목과 동일한 바이트를 가진 미검수 플러그인도 그 엄선된 것과 분리된 채로 유지된다 — 사이드로드는 결코 승인된 플러그인 행세를 하지 않는다.

GitHub를 임포트 소스로 삼기

두 경로 모두 플러그인 바이트를 동일한 파이프라인으로 통과시킨다:

  • 레지스트리 설치는 서버 프록시를 통해 제작자의 GitHub release에 호스팅된 아티팩트를 가져온 다음, 이를 고정된 SHA-256과 대조한다.
  • 사이드로드는 원문 GitHub URL을 받아, GitHub 계열 호스트로 제한한 다음, 동일한 방식으로 바이트를 대조한다.

두 경우 모두 GitHub release 에셋에 대한 브라우저의 CORS 제약은 서버 측에서 처리되며, 무결성은 코어가 무엇이든 로드하기 전 클라이언트에서의 해시 검사로 지켜진다.

Unity 안의 마켓

Unity 플러그인 마켓은 웹 마켓처럼 보이도록 UIToolkit으로 다시 만들어졌다 — 상세 패널 옆의 카드 그리드가, 동일한 정적 레지스트리를 읽으며, 동일한 해시 불일치 거부와 완전 신뢰 설치 안내를 갖춘다.

로그인과 브라우저에 보관되는 플러그인 목록·프리셋은 웹 전용으로 남는다 — Unity 창은 카탈로그와 설치 표면이다.

관련 페이지