Compare commits
5 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f94488e5ed | ||
|
|
9c4b53a6bc | ||
|
|
dcd0343569 | ||
|
|
8e33934951 | ||
|
|
d35af82aae |
25 changed files with 537 additions and 2811 deletions
226
.gitignore
vendored
226
.gitignore
vendored
|
|
@ -1,225 +1,3 @@
|
||||||
# Created by https://www.toptal.com/developers/gitignore/api/node,macos,windows,linux
|
node_modules
|
||||||
# Edit at https://www.toptal.com/developers/gitignore?templates=node,macos,windows,linux
|
|
||||||
|
|
||||||
### Linux ###
|
/dist
|
||||||
*~
|
|
||||||
|
|
||||||
# temporary files which can be created if a process still has a handle open of a deleted file
|
|
||||||
.fuse_hidden*
|
|
||||||
|
|
||||||
# KDE directory preferences
|
|
||||||
.directory
|
|
||||||
|
|
||||||
# Linux trash folder which might appear on any partition or disk
|
|
||||||
.Trash-*
|
|
||||||
|
|
||||||
# .nfs files are created when an open file is removed but is still being accessed
|
|
||||||
.nfs*
|
|
||||||
|
|
||||||
### macOS ###
|
|
||||||
# General
|
|
||||||
.DS_Store
|
|
||||||
.AppleDouble
|
|
||||||
.LSOverride
|
|
||||||
|
|
||||||
# Icon must end with two \r
|
|
||||||
Icon
|
|
||||||
|
|
||||||
|
|
||||||
# Thumbnails
|
|
||||||
._*
|
|
||||||
|
|
||||||
# Files that might appear in the root of a volume
|
|
||||||
.DocumentRevisions-V100
|
|
||||||
.fseventsd
|
|
||||||
.Spotlight-V100
|
|
||||||
.TemporaryItems
|
|
||||||
.Trashes
|
|
||||||
.VolumeIcon.icns
|
|
||||||
.com.apple.timemachine.donotpresent
|
|
||||||
|
|
||||||
# Directories potentially created on remote AFP share
|
|
||||||
.AppleDB
|
|
||||||
.AppleDesktop
|
|
||||||
Network Trash Folder
|
|
||||||
Temporary Items
|
|
||||||
.apdisk
|
|
||||||
|
|
||||||
### macOS Patch ###
|
|
||||||
# iCloud generated files
|
|
||||||
*.icloud
|
|
||||||
|
|
||||||
### Node ###
|
|
||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
.pnpm-debug.log*
|
|
||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
|
||||||
|
|
||||||
# Runtime data
|
|
||||||
pids
|
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
*.pid.lock
|
|
||||||
|
|
||||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
|
||||||
lib-cov
|
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
|
||||||
coverage
|
|
||||||
*.lcov
|
|
||||||
|
|
||||||
# nyc test coverage
|
|
||||||
.nyc_output
|
|
||||||
|
|
||||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
|
||||||
.grunt
|
|
||||||
|
|
||||||
# Bower dependency directory (https://bower.io/)
|
|
||||||
bower_components
|
|
||||||
|
|
||||||
# node-waf configuration
|
|
||||||
.lock-wscript
|
|
||||||
|
|
||||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
|
||||||
build/Release
|
|
||||||
|
|
||||||
# Dependency directories
|
|
||||||
node_modules/
|
|
||||||
jspm_packages/
|
|
||||||
|
|
||||||
# Snowpack dependency directory (https://snowpack.dev/)
|
|
||||||
web_modules/
|
|
||||||
|
|
||||||
# TypeScript cache
|
|
||||||
*.tsbuildinfo
|
|
||||||
|
|
||||||
# Optional npm cache directory
|
|
||||||
.npm
|
|
||||||
|
|
||||||
# Optional eslint cache
|
|
||||||
.eslintcache
|
|
||||||
|
|
||||||
# Optional stylelint cache
|
|
||||||
.stylelintcache
|
|
||||||
|
|
||||||
# Microbundle cache
|
|
||||||
.rpt2_cache/
|
|
||||||
.rts2_cache_cjs/
|
|
||||||
.rts2_cache_es/
|
|
||||||
.rts2_cache_umd/
|
|
||||||
|
|
||||||
# Optional REPL history
|
|
||||||
.node_repl_history
|
|
||||||
|
|
||||||
# Output of 'npm pack'
|
|
||||||
*.tgz
|
|
||||||
|
|
||||||
# Yarn Integrity file
|
|
||||||
.yarn-integrity
|
|
||||||
|
|
||||||
# dotenv environment variable files
|
|
||||||
.env
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
.env.local
|
|
||||||
|
|
||||||
# parcel-bundler cache (https://parceljs.org/)
|
|
||||||
.cache
|
|
||||||
.parcel-cache
|
|
||||||
|
|
||||||
# Next.js build output
|
|
||||||
.next
|
|
||||||
out
|
|
||||||
|
|
||||||
# Nuxt.js build / generate output
|
|
||||||
.nuxt
|
|
||||||
dist
|
|
||||||
|
|
||||||
# Gatsby files
|
|
||||||
.cache/
|
|
||||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
|
||||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
|
||||||
# public
|
|
||||||
|
|
||||||
# vuepress build output
|
|
||||||
.vuepress/dist
|
|
||||||
|
|
||||||
# vuepress v2.x temp and cache directory
|
|
||||||
.temp
|
|
||||||
|
|
||||||
# Docusaurus cache and generated files
|
|
||||||
.docusaurus
|
|
||||||
|
|
||||||
# Serverless directories
|
|
||||||
.serverless/
|
|
||||||
|
|
||||||
# FuseBox cache
|
|
||||||
.fusebox/
|
|
||||||
|
|
||||||
# DynamoDB Local files
|
|
||||||
.dynamodb/
|
|
||||||
|
|
||||||
# TernJS port file
|
|
||||||
.tern-port
|
|
||||||
|
|
||||||
# Stores VSCode versions used for testing VSCode extensions
|
|
||||||
.vscode-test
|
|
||||||
|
|
||||||
# yarn v2
|
|
||||||
.yarn/cache
|
|
||||||
.yarn/unplugged
|
|
||||||
.yarn/build-state.yml
|
|
||||||
.yarn/install-state.gz
|
|
||||||
.pnp.*
|
|
||||||
|
|
||||||
### Node Patch ###
|
|
||||||
# Serverless Webpack directories
|
|
||||||
.webpack/
|
|
||||||
|
|
||||||
# Optional stylelint cache
|
|
||||||
|
|
||||||
# SvelteKit build / generate output
|
|
||||||
.svelte-kit
|
|
||||||
|
|
||||||
### Windows ###
|
|
||||||
# Windows thumbnail cache files
|
|
||||||
Thumbs.db
|
|
||||||
Thumbs.db:encryptable
|
|
||||||
ehthumbs.db
|
|
||||||
ehthumbs_vista.db
|
|
||||||
|
|
||||||
# Dump file
|
|
||||||
*.stackdump
|
|
||||||
|
|
||||||
# Folder config file
|
|
||||||
[Dd]esktop.ini
|
|
||||||
|
|
||||||
# Recycle Bin used on file shares
|
|
||||||
$RECYCLE.BIN/
|
|
||||||
|
|
||||||
# Windows Installer files
|
|
||||||
*.cab
|
|
||||||
*.msi
|
|
||||||
*.msix
|
|
||||||
*.msm
|
|
||||||
*.msp
|
|
||||||
|
|
||||||
# Windows shortcuts
|
|
||||||
*.lnk
|
|
||||||
|
|
||||||
#!dist/
|
|
||||||
dist/*
|
|
||||||
packages/frontend/dist
|
|
||||||
packages/backend/dist
|
|
||||||
#!dist/*.zip
|
|
||||||
dist/plugin_package.zip
|
|
||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/node,macos,windows,linux
|
|
||||||
|
|
|
||||||
2
LICENSE
2
LICENSE
|
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2024 [Author]
|
Copyright (c) 2024 [caterpii]
|
||||||
|
|
||||||
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:
|
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:
|
||||||
|
|
||||||
|
|
|
||||||
21
README.md
21
README.md
|
|
@ -1,6 +1,19 @@
|
||||||
# caido-plugin-test
|
# caido-plugin-test
|
||||||
|
|
||||||
```bash
|
## To-Do
|
||||||
pnpm install
|
- [ ] PKCE 다운그래이드 https에서 작동 안하는 이슈 고치기
|
||||||
pnpm run watch
|
|
||||||
```
|
```log
|
||||||
|
2025-05-25T15:52:40.757475Z INFO actix-rt|system:0|arbiter:6 proxy|connect: Client connection (29e74afd-9006-445e-88a9-3fc5d4796af9)
|
||||||
|
2025-05-25T15:52:40.757530Z INFO actix-rt|system:0|arbiter:6 proxy|connect: Client connected for http://localhost:8787 (29e74afd-9006-445e-88a9-3fc5d4796af9)
|
||||||
|
2025-05-25T15:52:40.757562Z INFO actix-rt|system:0|arbiter:6 proxy|http1|logger: GET http://localhost/login (29e74afd-9006-445e-88a9-3fc5d4796af9)
|
||||||
|
2025-05-25T15:52:40.767186Z INFO actix-rt|system:0|arbiter:6 proxy|http1|logger: GET http://localhost:8787/login -> 302 361 (29e74afd-9006-445e-88a9-3fc5d4796af9)
|
||||||
|
2025-05-25T15:52:40.768696Z INFO actix-rt|system:0|arbiter:9 proxy|http1|logger: GET https://github.com/login/oauth/authorize?client_id=Ov23lixietSCQOHxPvcr&redirect_uri=http%3A%2F%2Flocalhost%3A8787%2Fcallback&scope=read%3Auser&state=bc11db571a4737d0&response_type=code&code_challenge=FtSdQsWI342PKH6BGgKYR6AOzW95LaS0jeVcwTmHaro&code_challenge_method=S256 (90f314dc-9480-4bd8-b7b6-5acba6b8bc7b)
|
||||||
|
2025-05-25T15:52:41.103596Z INFO actix-rt|system:0|arbiter:9 proxy|http1|logger: GET https://github.com/login/oauth/authorize?client_id=Ov23lixietSCQOHxPvcr&redirect_uri=http%3A%2F%2Flocalhost%3A8787%2Fcallback&scope=read%3Auser&state=bc11db571a4737d0&response_type=code&code_challenge=FtSdQsWI342PKH6BGgKYR6AOzW95LaS0jeVcwTmHaro&code_challenge_method=S256 -> 302 4927 (90f314dc-9480-4bd8-b7b6-5acba6b8bc7b)
|
||||||
|
2025-05-25T15:52:41.105944Z INFO actix-rt|system:0|arbiter:7 proxy|connect: Client connection (34585a00-9f9f-4c72-b087-2e9e92418dad)
|
||||||
|
2025-05-25T15:52:41.105993Z INFO actix-rt|system:0|arbiter:7 proxy|connect: Client connected for http://localhost:8787 (34585a00-9f9f-4c72-b087-2e9e92418dad)
|
||||||
|
2025-05-25T15:52:41.106023Z INFO actix-rt|system:0|arbiter:7 proxy|http1|logger: GET http://localhost/callback?code=10c34dcc4d3f7302e707&state=bc11db571a4737d0 (34585a00-9f9f-4c72-b087-2e9e92418dad)
|
||||||
|
2025-05-25T15:52:41.108270Z INFO plugin:65ad3a87-0257-4408-a9c7-e0885e04c162 js|sdk: [PKCEDowngradeCheck] Required PKCE parameters missing. Skipping.
|
||||||
|
2025-05-25T15:52:41.277387Z INFO plugin:65ad3a87-0257-4408-a9c7-e0885e04c162 js|sdk: [PKCEDowngradeCheck] No PKCE downgrade detected.
|
||||||
|
2025-05-25T15:52:41.686109Z INFO actix-rt|system:0|arbiter:7 proxy|http1|logger: GET http://localhost:8787/callback?code=10c34dcc4d3f7302e707&state=bc11db571a4737d0 -> 200 1582 (34585a00-9f9f-4c72-b087-2e9e92418dad)
|
||||||
|
```
|
||||||
BIN
build.zip
Normal file
BIN
build.zip
Normal file
Binary file not shown.
52
bun.lock
52
bun.lock
|
|
@ -3,10 +3,6 @@
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "caido-oauth",
|
"name": "caido-oauth",
|
||||||
"dependencies": {
|
|
||||||
"@types/jsonwebtoken": "^9.0.9",
|
|
||||||
"jsonwebtoken": "^9.0.2",
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@caido-community/dev": "^0.1.3",
|
"@caido-community/dev": "^0.1.3",
|
||||||
"@caido/sdk-backend": "^0.48.1",
|
"@caido/sdk-backend": "^0.48.1",
|
||||||
|
|
@ -131,12 +127,6 @@
|
||||||
|
|
||||||
"@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
|
"@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
|
||||||
|
|
||||||
"@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.9", "", { "dependencies": { "@types/ms": "*", "@types/node": "*" } }, "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ=="],
|
|
||||||
|
|
||||||
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
|
||||||
|
|
||||||
"@types/node": ["@types/node@22.15.29", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ=="],
|
|
||||||
|
|
||||||
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||||
|
|
||||||
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
||||||
|
|
@ -153,8 +143,6 @@
|
||||||
|
|
||||||
"brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
"brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
||||||
|
|
||||||
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
|
||||||
|
|
||||||
"bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="],
|
"bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="],
|
||||||
|
|
||||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||||
|
|
@ -197,8 +185,6 @@
|
||||||
|
|
||||||
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
|
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
|
||||||
|
|
||||||
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
|
||||||
|
|
||||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||||
|
|
||||||
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||||
|
|
@ -275,14 +261,8 @@
|
||||||
|
|
||||||
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||||
|
|
||||||
"jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="],
|
|
||||||
|
|
||||||
"jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="],
|
"jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="],
|
||||||
|
|
||||||
"jwa": ["jwa@1.4.2", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw=="],
|
|
||||||
|
|
||||||
"jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="],
|
|
||||||
|
|
||||||
"lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="],
|
"lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="],
|
||||||
|
|
||||||
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
|
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
|
||||||
|
|
@ -291,20 +271,6 @@
|
||||||
|
|
||||||
"load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="],
|
"load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="],
|
||||||
|
|
||||||
"lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
|
|
||||||
|
|
||||||
"lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="],
|
|
||||||
|
|
||||||
"lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="],
|
|
||||||
|
|
||||||
"lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="],
|
|
||||||
|
|
||||||
"lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="],
|
|
||||||
|
|
||||||
"lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="],
|
|
||||||
|
|
||||||
"lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="],
|
|
||||||
|
|
||||||
"lodash.sortby": ["lodash.sortby@4.7.0", "", {}, "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA=="],
|
"lodash.sortby": ["lodash.sortby@4.7.0", "", {}, "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA=="],
|
||||||
|
|
||||||
"lru-cache": ["lru-cache@11.1.0", "", {}, "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A=="],
|
"lru-cache": ["lru-cache@11.1.0", "", {}, "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A=="],
|
||||||
|
|
@ -325,7 +291,7 @@
|
||||||
|
|
||||||
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="],
|
||||||
|
|
||||||
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
|
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
|
||||||
|
|
||||||
|
|
@ -391,8 +357,6 @@
|
||||||
|
|
||||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||||
|
|
||||||
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
|
||||||
|
|
||||||
"send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="],
|
"send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="],
|
||||||
|
|
||||||
"serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="],
|
"serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="],
|
||||||
|
|
@ -455,8 +419,6 @@
|
||||||
|
|
||||||
"typescript": ["typescript@5.5.4", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q=="],
|
"typescript": ["typescript@5.5.4", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
|
||||||
|
|
||||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||||
|
|
||||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||||
|
|
@ -487,14 +449,14 @@
|
||||||
|
|
||||||
"body-parser/qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
|
"body-parser/qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
|
||||||
|
|
||||||
"debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="],
|
|
||||||
|
|
||||||
"finalhandler/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
"finalhandler/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||||
|
|
||||||
"readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
|
"readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
|
||||||
|
|
||||||
"router/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
"router/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||||
|
|
||||||
|
"send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
"string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
"string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
@ -515,6 +477,12 @@
|
||||||
|
|
||||||
"wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
"wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
|
"body-parser/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"finalhandler/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"router/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
"sucrase/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
"sucrase/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
||||||
|
|
@ -523,6 +491,8 @@
|
||||||
|
|
||||||
"sucrase/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
"sucrase/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
||||||
|
|
||||||
|
"tsup/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
"wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
"wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,24 @@
|
||||||
import { defineConfig } from "@caido-community/dev";
|
import { defineConfig } from '@caido-community/dev';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
id: "caido-oauth",
|
id: "oauth-vuln-detector",
|
||||||
name: "Caido OAuth",
|
name: "OAuth Vuln Detector",
|
||||||
description: "Plugin for OAuth",
|
description: "Detects OAuth misconfiguration.",
|
||||||
version: "0.0.0",
|
version: "0.0.1",
|
||||||
author: {
|
author: {
|
||||||
name: "WHS Safe Us Team",
|
name: "caterpii",
|
||||||
|
email: "dlaha171@gmail.com",
|
||||||
|
url: "https://github.com/katerpii"
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
{
|
{
|
||||||
kind: "backend",
|
kind: "backend",
|
||||||
id: "backend",
|
id: "oauth-backend",
|
||||||
root: "packages/backend",
|
root: "packages/backend",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
watch: {
|
||||||
|
port: 8081,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "caido-oauth-dev",
|
"name": "caido-oauth-plugin",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
@ -9,11 +9,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@caido-community/dev": "^0.1.3",
|
"@caido-community/dev": "^0.1.3",
|
||||||
"@caido/sdk-backend": "^0.48.1",
|
"@caido/sdk-backend": "^0.46.0",
|
||||||
"typescript": "5.5.4"
|
"typescript": "5.5.4"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@types/jsonwebtoken": "^9.0.9",
|
|
||||||
"jsonwebtoken": "^9.0.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
190
packages/backend/dist/index.js
vendored
Normal file
190
packages/backend/dist/index.js
vendored
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
// packages/backend/src/controller/PKCECheck.ts
|
||||||
|
import { RequestSpec } from "caido:utils";
|
||||||
|
var PKCECheck = class {
|
||||||
|
async test(sdk, req) {
|
||||||
|
const method = req.getMethod();
|
||||||
|
if (method !== "GET") {
|
||||||
|
sdk.console.log("[PKCEDowngradeCheck] Not a GET request. Skipping.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const query = req.getQuery();
|
||||||
|
const searchParams = new URLSearchParams(query);
|
||||||
|
const requiredKeys = ["client_id", "response_type", "code_challenge", "code_challenge_method"];
|
||||||
|
if (!requiredKeys.every((key) => searchParams.has(key))) {
|
||||||
|
sdk.console.log("[PKCEDowngradeCheck] Required PKCE parameters missing. Skipping.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const url = req.getUrl();
|
||||||
|
const isOpenID = searchParams.get("scope")?.includes("openid") || url.includes("id_token");
|
||||||
|
const methodVal = searchParams.get("code_challenge_method");
|
||||||
|
const challengeVal = searchParams.get("code_challenge");
|
||||||
|
if (!methodVal || !challengeVal) {
|
||||||
|
sdk.console.log("[PKCEDowngradeCheck] code_challenge or method missing. Skipping.");
|
||||||
|
await sdk.findings.create({
|
||||||
|
title: isOpenID ? "[WARN] OpenID Flow PKCE Parameters Missing" : "[WARN] OAuth2 Flow PKCE Parameters Missing",
|
||||||
|
description: `PKCE parameters are missing or incomplete for ${url}. This may indicate a misconfiguration.`,
|
||||||
|
request: req,
|
||||||
|
reporter: "PKCE Checker"
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (methodVal === "plain") {
|
||||||
|
sdk.console.log("[PKCEDowngradeCheck] code_challenge_method is 'plain'. Skipping.");
|
||||||
|
await sdk.findings.create({
|
||||||
|
title: isOpenID ? "[WARN] OpenID Flow PKCE Method is 'plain'" : "[WARN] OAuth2 Flow PKCE Method is 'plain'",
|
||||||
|
description: `PKCE method is set to 'plain' for ${url}. This may indicate a downgrade vulnerability.`,
|
||||||
|
request: req,
|
||||||
|
reporter: "PKCE Checker"
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
searchParams.delete("code_challenge");
|
||||||
|
searchParams.delete("code_challenge_method");
|
||||||
|
const downgradedQuery = searchParams.toString();
|
||||||
|
const scheme = req.getUrl().startsWith("https") ? "https" : "http";
|
||||||
|
const downgradedUrl = `${scheme}://${req.getHost()}:${req.getPort()}${req.getPath()}?${downgradedQuery}`;
|
||||||
|
sdk.console.log(`${req.getHost()} Original URL: ` + url);
|
||||||
|
sdk.console.log(`${req.getHost()} Downgraded URL: ` + downgradedUrl);
|
||||||
|
try {
|
||||||
|
const spec = new RequestSpec(downgradedUrl);
|
||||||
|
spec.setBody(req.getBody());
|
||||||
|
for (const [key, value] of Object.entries(req.getHeaders())) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
spec.setHeader(key, value.join(", "));
|
||||||
|
} else {
|
||||||
|
spec.setHeader(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
spec.setHost(req.getHost());
|
||||||
|
spec.setMethod(req.getMethod());
|
||||||
|
spec.setPath(req.getPath());
|
||||||
|
spec.setQuery(downgradedQuery);
|
||||||
|
spec.setTls(req.getTls());
|
||||||
|
spec.setPort(req.getPort());
|
||||||
|
let sendDowngradedRequest = await sdk.requests.send(spec);
|
||||||
|
if (sendDowngradedRequest.response) {
|
||||||
|
let domain = spec.getHost();
|
||||||
|
let port = spec.getPort();
|
||||||
|
let path2 = spec.getPath();
|
||||||
|
let query2 = spec.getQuery();
|
||||||
|
let id = sendDowngradedRequest.response.getId();
|
||||||
|
let code = sendDowngradedRequest.response.getCode();
|
||||||
|
sdk.console.log(`REQ ${id}: ${domain}:${port}${path2}${query2} received a status code of ${code}`);
|
||||||
|
}
|
||||||
|
if (sendDowngradedRequest.response?.getCode() === 302) {
|
||||||
|
await sdk.findings.create({
|
||||||
|
title: isOpenID ? "[CRITICAL] OpenID Flow PKCE Downgrade Vulnerability" : "[CRITICAL] OAuth2 Flow PKCE Downgrade Vulnerability",
|
||||||
|
description: `The request to ${url} is vulnerable to a PKCE downgrade attack. This may indicate a configuration error.`,
|
||||||
|
request: req,
|
||||||
|
reporter: "PKCE Checker"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
sdk.console.error(`PKCE downgrade check failed for ${url}: ${String(err)}`);
|
||||||
|
}
|
||||||
|
sdk.console.log("[PKCEDowngradeCheck] No PKCE downgrade detected.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// packages/backend/src/controller/redirectUriCheck.ts
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
import * as path from "path";
|
||||||
|
import os from "os";
|
||||||
|
var redirectUriCheck = class {
|
||||||
|
requestMap = /* @__PURE__ */ new Map();
|
||||||
|
// constructor(private sdk: SDK<any>) {}
|
||||||
|
async onRequest(sdk, req) {
|
||||||
|
try {
|
||||||
|
const urlString = req.getUrl();
|
||||||
|
const url = new URL(urlString);
|
||||||
|
sdk.console.log(`[OAuthPlugin] Intercepted request: ${urlString}`);
|
||||||
|
if (!url.pathname.includes("/authorize") && !url.pathname.includes("/auth")) return;
|
||||||
|
const params = new URLSearchParams(url.search);
|
||||||
|
const redirectUri = params.get("redirect_uri");
|
||||||
|
if (!redirectUri) return;
|
||||||
|
const reqId = req.getId();
|
||||||
|
this.requestMap.set(reqId, redirectUri);
|
||||||
|
const clientId = params.get("client_id") ?? "(missing)";
|
||||||
|
const responseType = params.get("response_type") ?? "(missing)";
|
||||||
|
const isScan = params.has("scan");
|
||||||
|
if (isScan) return;
|
||||||
|
const output = {
|
||||||
|
original_url: urlString,
|
||||||
|
client_id: clientId,
|
||||||
|
redirect_uri: redirectUri,
|
||||||
|
response_type: responseType
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const filePath = path.join(os.tmpdir(), "oauth-fuzz-input.json");
|
||||||
|
await fs.writeFile(filePath, JSON.stringify(output, null, 2));
|
||||||
|
} catch (err) {
|
||||||
|
await sdk.findings.create({
|
||||||
|
title: "[fs] Write Failed",
|
||||||
|
description: `Could not write to file: ${err}`,
|
||||||
|
request: req,
|
||||||
|
reporter: "oauth-open-redirect-detector"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await sdk.findings.create({
|
||||||
|
title: "[ ] OAuth2 Authorization Request Collected",
|
||||||
|
description: `client_id: ${clientId}
|
||||||
|
redirect_uri: ${redirectUri}
|
||||||
|
response_type: ${responseType}`,
|
||||||
|
request: req,
|
||||||
|
reporter: "oauth-open-redirect-detector"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
sdk.console.error(`Error in onRequest: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async onResponse(sdk, req, resp) {
|
||||||
|
try {
|
||||||
|
const reqId = req.getId();
|
||||||
|
const url = new URL(req.getUrl());
|
||||||
|
const status = resp.getCode();
|
||||||
|
const location = resp.getHeader("location")?.[0];
|
||||||
|
const params = new URLSearchParams(url.search);
|
||||||
|
const isScan = params.has("scan");
|
||||||
|
if (!isScan) {
|
||||||
|
this.requestMap.delete(reqId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (status >= 300 && status < 400 && location) {
|
||||||
|
const redirectUri = this.requestMap.get(reqId) ?? "(unknown)";
|
||||||
|
await sdk.findings.create({
|
||||||
|
title: "[+] Redirect URI Misconfiguration Detected",
|
||||||
|
description: `Status: ${status}
|
||||||
|
Location: ${location}
|
||||||
|
Request URL: ${url.href}
|
||||||
|
Redirect URI: ${redirectUri}`,
|
||||||
|
request: req,
|
||||||
|
reporter: "oauth-open-redirect-detector"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.requestMap.delete(reqId);
|
||||||
|
} catch (err) {
|
||||||
|
sdk.console.error(`Error in onResponse: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// packages/backend/src/index.ts
|
||||||
|
var pkceCheckController = new PKCECheck();
|
||||||
|
var redirectUriCheckController = new redirectUriCheck();
|
||||||
|
function init(sdk) {
|
||||||
|
sdk.events.onInterceptRequest(
|
||||||
|
async (sdk2, req) => {
|
||||||
|
await redirectUriCheckController.onRequest(sdk2, req);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
sdk.events.onInterceptResponse(
|
||||||
|
async (sdk2, req, resp) => {
|
||||||
|
await pkceCheckController.test(sdk2, req);
|
||||||
|
await redirectUriCheckController.onResponse(sdk2, req, resp);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
init
|
||||||
|
};
|
||||||
|
|
@ -6,8 +6,5 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"build": "vite build"
|
"build": "vite build"
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@caido/sdk-backend": "^0.46.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
35
packages/backend/pnpm-lock.yaml
generated
Normal file
35
packages/backend/pnpm-lock.yaml
generated
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
lockfileVersion: '9.0'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
autoInstallPeers: true
|
||||||
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
importers:
|
||||||
|
|
||||||
|
.:
|
||||||
|
devDependencies:
|
||||||
|
'@caido/sdk-backend':
|
||||||
|
specifier: ^0.46.0
|
||||||
|
version: 0.46.0
|
||||||
|
|
||||||
|
packages:
|
||||||
|
|
||||||
|
'@caido/quickjs-types@0.17.2':
|
||||||
|
resolution: {integrity: sha512-5kcucGORMNEbcdU91yKLYZG/TFDqsO6XmCZ1TnU6V48E61mmqrJg6kjrfOFP1WOugDm+ZcGd/Su3p3XkFXfaPg==}
|
||||||
|
|
||||||
|
'@caido/sdk-backend@0.46.0':
|
||||||
|
resolution: {integrity: sha512-peUKW/4Nrw9WVxIahc+6KrVtxA7vsbpuJqOoBxudxq7tQJ+cV9IEqzvYoFFo8KlnrTkeUQUJvd0W4WsM3HgxEg==}
|
||||||
|
|
||||||
|
'@caido/sdk-shared@0.1.1':
|
||||||
|
resolution: {integrity: sha512-JAV5ajUqxZdXYPTmDEvIKBZon8I5uHq44ATj0Nj3BVpllRDUGY9kcBd+PXMD50+3lv1CvhR3/f6q24T0+4aVJQ==}
|
||||||
|
|
||||||
|
snapshots:
|
||||||
|
|
||||||
|
'@caido/quickjs-types@0.17.2': {}
|
||||||
|
|
||||||
|
'@caido/sdk-backend@0.46.0':
|
||||||
|
dependencies:
|
||||||
|
'@caido/quickjs-types': 0.17.2
|
||||||
|
'@caido/sdk-shared': 0.1.1
|
||||||
|
|
||||||
|
'@caido/sdk-shared@0.1.1': {}
|
||||||
|
|
@ -2,94 +2,138 @@ import type { SDK } from "caido:plugin";
|
||||||
import { Body, RequestSpec, type Request } from "caido:utils";
|
import { Body, RequestSpec, type Request } from "caido:utils";
|
||||||
|
|
||||||
export class PKCECheck {
|
export class PKCECheck {
|
||||||
// 필요한 PKCE 파라미터 목록
|
|
||||||
private readonly requiredPKCEKeys = ["client_id", "response_type", "code_challenge", "code_challenge_method"];
|
|
||||||
|
|
||||||
// PKCE 취약점 테스트 메인 함수
|
|
||||||
async test(sdk: SDK, req: Request): Promise<boolean> {
|
async test(sdk: SDK, req: Request): Promise<boolean> {
|
||||||
const method = req.getMethod();
|
const method = req.getMethod();
|
||||||
const url = req.getUrl();
|
|
||||||
|
|
||||||
// GET 요청이 아니면 검사하지 않음
|
|
||||||
if (method !== "GET") {
|
if (method !== "GET") {
|
||||||
sdk.console.log("[PKCEDowngradeCheck] Not a GET request. Skipping.");
|
sdk.console.log("[PKCEDowngradeCheck] Not a GET request. Skipping.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchParams = new URLSearchParams(req.getQuery());
|
const query = req.getQuery();
|
||||||
|
const searchParams = new URLSearchParams(query);
|
||||||
|
const requiredKeys = ["client_id", "response_type", "code_challenge", "code_challenge_method"];
|
||||||
|
|
||||||
// 필수 PKCE 파라미터들이 모두 있는지 확인
|
if (!requiredKeys.every((key) => searchParams.has(key))) {
|
||||||
if (!this.requiredPKCEKeys.every(key => searchParams.has(key))) {
|
|
||||||
sdk.console.log("[PKCEDowngradeCheck] Required PKCE parameters missing. Skipping.");
|
sdk.console.log("[PKCEDowngradeCheck] Required PKCE parameters missing. Skipping.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenID 여부 확인
|
const url = req.getUrl();
|
||||||
const isOpenID = searchParams.get("scope")?.includes("openid") || url.includes("id_token");
|
const isOpenID = searchParams.get("scope")?.includes("openid") || url.includes("id_token");
|
||||||
const methodVal = searchParams.get("code_challenge_method");
|
const methodVal = searchParams.get("code_challenge_method");
|
||||||
const challengeVal = searchParams.get("code_challenge");
|
const challengeVal = searchParams.get("code_challenge");
|
||||||
|
|
||||||
// 파라미터가 없으면 경고 리포트 생성
|
|
||||||
if (!methodVal || !challengeVal) {
|
if (!methodVal || !challengeVal) {
|
||||||
await this.reportFinding(sdk, req, url, isOpenID, "[WARN] PKCE Parameters Missing", "PKCE parameters are missing or incomplete.");
|
sdk.console.log("[PKCEDowngradeCheck] code_challenge or method missing. Skipping.");
|
||||||
|
await sdk.findings.create({
|
||||||
|
title: isOpenID
|
||||||
|
? "[WARN] OpenID Flow PKCE Parameters Missing"
|
||||||
|
: "[WARN] OAuth2 Flow PKCE Parameters Missing",
|
||||||
|
description: `PKCE parameters are missing or incomplete for ${url}. This may indicate a misconfiguration.`,
|
||||||
|
request: req,
|
||||||
|
reporter: "PKCE Checker",
|
||||||
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// code_challenge_method가 'plain'이면 취약할 수 있음
|
|
||||||
if (methodVal === "plain") {
|
if (methodVal === "plain") {
|
||||||
await this.reportFinding(sdk, req, url, isOpenID, "[WARN] PKCE Method is 'plain'", "PKCE method is set to 'plain'. This may indicate a downgrade vulnerability.");
|
sdk.console.log("[PKCEDowngradeCheck] code_challenge_method is 'plain'. Skipping.");
|
||||||
|
await sdk.findings.create({
|
||||||
|
title: isOpenID
|
||||||
|
? "[WARN] OpenID Flow PKCE Method is 'plain'"
|
||||||
|
: "[WARN] OAuth2 Flow PKCE Method is 'plain'",
|
||||||
|
description: `PKCE method is set to 'plain' for ${url}. This may indicate a downgrade vulnerability.`,
|
||||||
|
request: req,
|
||||||
|
reporter: "PKCE Checker",
|
||||||
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// PKCE 관련 파라미터 제거하여 다운그레이드된 URL 생성
|
// Remove PKCE parameters to simulate a downgraded request
|
||||||
searchParams.delete("code_challenge");
|
searchParams.delete("code_challenge");
|
||||||
searchParams.delete("code_challenge_method");
|
searchParams.delete("code_challenge_method");
|
||||||
const downgradedQuery = searchParams.toString();
|
const downgradedQuery = searchParams.toString();
|
||||||
const scheme = url.startsWith("https") ? "https" : "http";
|
const scheme = req.getUrl().startsWith("https") ? "https" : "http";
|
||||||
const downgradedUrl = `${scheme}://${req.getHost()}:${req.getPort()}${req.getPath()}?${downgradedQuery}`;
|
const downgradedUrl = `${scheme}://${req.getHost()}:${req.getPort()}${req.getPath()}?${downgradedQuery}`;
|
||||||
|
|
||||||
sdk.console.log(`${req.getHost()} Original URL: ${url}`);
|
sdk.console.log(`${req.getHost()} Original URL: ` + url);
|
||||||
sdk.console.log(`${req.getHost()} Downgraded URL: ${downgradedUrl}`);
|
sdk.console.log(`${req.getHost()} Downgraded URL: ` + downgradedUrl);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 원래 요청과 다운그레이드된 요청 각각 전송
|
// Use Caido Replay SDK to replay the original request
|
||||||
const downgradedResponse = await this.sendRequest(sdk, req, downgradedUrl, downgradedQuery);
|
const spec = new RequestSpec(downgradedUrl);
|
||||||
const originalResponse = await this.sendRequest(sdk, req, url, req.getQuery());
|
spec.setBody(req.getBody() as Body);
|
||||||
|
for (const [key, value] of Object.entries(req.getHeaders())) {
|
||||||
if (downgradedResponse && originalResponse) {
|
if (Array.isArray(value)) {
|
||||||
const originalCode = originalResponse.getCode();
|
spec.setHeader(key, value.join(', ')); // or another suitable delimiter
|
||||||
const downgradedCode = downgradedResponse.getCode();
|
} else {
|
||||||
|
spec.setHeader(key, value);
|
||||||
const originalLoc = originalResponse.getHeader("location") || "";
|
|
||||||
const downgradedLoc = downgradedResponse.getHeader("location") || "";
|
|
||||||
|
|
||||||
sdk.console.log(`${req.getHost()} Original Status: ${originalCode}`);
|
|
||||||
sdk.console.log(`${req.getHost()} Downgraded Status: ${downgradedCode}`);
|
|
||||||
sdk.console.log(`${req.getHost()} Original Location: ${originalLoc}`);
|
|
||||||
sdk.console.log(`${req.getHost()} Downgraded Location: ${downgradedLoc}`);
|
|
||||||
|
|
||||||
// 두 응답 모두 리디렉션이면서 code= 파라미터 포함 시 취약점 리포트 생성
|
|
||||||
const bothRedirect = [301, 302].includes(originalCode) && [301, 302].includes(downgradedCode);
|
|
||||||
const bothContainCode = originalLoc.includes("code=") && downgradedLoc.includes("code=");
|
|
||||||
|
|
||||||
if (bothRedirect && bothContainCode) {
|
|
||||||
const title = isOpenID
|
|
||||||
? "[CRITICAL] OpenID Flow PKCE Downgrade Vulnerability"
|
|
||||||
: "[CRITICAL] OAuth2 Flow PKCE Downgrade Vulnerability";
|
|
||||||
const reference = isOpenID
|
|
||||||
? "https://openid.net/specs/openid-igov-oauth2-1_0-02.html#rfc.section.3.1.7"
|
|
||||||
: "https://datatracker.ietf.org/doc/html/rfc7636";
|
|
||||||
|
|
||||||
await sdk.findings.create({
|
|
||||||
title,
|
|
||||||
description: `PKCE downgrade vulnerability detected!\n\nOriginal URL: ${url}\nDowngraded URL: ${downgradedUrl}\n\nBoth requests returned authorization codes, indicating the server accepts requests without PKCE protection.\n\nReference: ${reference}`,
|
|
||||||
request: req,
|
|
||||||
reporter: "PKCE Checker",
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
spec.setHost(req.getHost());
|
||||||
|
spec.setMethod(req.getMethod());
|
||||||
|
spec.setPath(req.getPath());
|
||||||
|
spec.setQuery(downgradedQuery);
|
||||||
|
spec.setTls(req.getTls());
|
||||||
|
spec.setPort(req.getPort());
|
||||||
|
|
||||||
|
let sendDowngradedRequest = await sdk.requests.send(spec);
|
||||||
|
|
||||||
|
if (sendDowngradedRequest.response) {
|
||||||
|
let domain = spec.getHost();
|
||||||
|
let port = spec.getPort();
|
||||||
|
let path = spec.getPath();
|
||||||
|
let query = spec.getQuery();
|
||||||
|
let id = sendDowngradedRequest.response.getId();
|
||||||
|
let code = sendDowngradedRequest.response.getCode();
|
||||||
|
sdk.console.log(`REQ ${id}: ${domain}:${port}${path}${query} received a status code of ${code}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sendDowngradedRequest.response?.getCode() === 302) {
|
||||||
|
await sdk.findings.create({
|
||||||
|
title: isOpenID
|
||||||
|
? "[CRITICAL] OpenID Flow PKCE Downgrade Vulnerability"
|
||||||
|
: "[CRITICAL] OAuth2 Flow PKCE Downgrade Vulnerability",
|
||||||
|
description: `The request to ${url} is vulnerable to a PKCE downgrade attack. This may indicate a configuration error.`,
|
||||||
|
request: req,
|
||||||
|
reporter: "PKCE Checker",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
sdk.console.log(`${req.getHost()} Original Status: ` + resOriginal.status);
|
||||||
|
sdk.console.log(`${req.getHost()} Downgraded Status: ` + resDowngraded.status);
|
||||||
|
|
||||||
|
sdk.console.log(`${req.getHost()} Original Headers: ` + JSON.stringify(resOriginal.headers));
|
||||||
|
sdk.console.log(`${req.getHost()} Downgraded Headers: ` + JSON.stringify(resDowngraded.headers));
|
||||||
|
|
||||||
|
// Caido Dev Docs 기준으로, 리다이렉트된 URL은 Response 객체의 url 속성에 저장되어 있음
|
||||||
|
const locationOriginal = resOriginal.url ?? "";
|
||||||
|
const locationDowngraded = resDowngraded.url ?? "";
|
||||||
|
|
||||||
|
sdk.console.log(`${req.getHost()} Original Location: ` + locationOriginal);
|
||||||
|
sdk.console.log(`${req.getHost()} Downgraded Location: ` + locationDowngraded);
|
||||||
|
|
||||||
|
const statusEqual = resOriginal.status === resDowngraded.status;
|
||||||
|
const codeInRedirects = locationOriginal.includes("code=") && locationDowngraded.includes("code=");
|
||||||
|
|
||||||
|
if (statusEqual && codeInRedirects) {
|
||||||
|
const title = isOpenID
|
||||||
|
? "[CRITICAL] OpenID Flow PKCE Downgraded to Plaintext"
|
||||||
|
: "[CRITICAL] OAuth2 Flow PKCE Downgraded to Plaintext";
|
||||||
|
const reference = isOpenID
|
||||||
|
? "https://openid.net/specs/openid-igov-oauth2-1_0-02.html#rfc.section.3.1.7"
|
||||||
|
: "https://datatracker.ietf.org/doc/html/rfc7636";
|
||||||
|
|
||||||
|
await sdk.findings.create({
|
||||||
|
title,
|
||||||
|
description: `PKCE downgrade detected for ${url}.\n\nDowngraded URL: ${downgradedUrl}\n\nRedirect contained code=.\n\nReference: ${reference}`,
|
||||||
|
request: req,
|
||||||
|
reporter: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}*/
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
sdk.console.error(`PKCE downgrade check failed for ${url}: ${String(err)}`);
|
sdk.console.error(`PKCE downgrade check failed for ${url}: ${String(err)}`);
|
||||||
}
|
}
|
||||||
|
|
@ -97,41 +141,4 @@ export class PKCECheck {
|
||||||
sdk.console.log("[PKCEDowngradeCheck] No PKCE downgrade detected.");
|
sdk.console.log("[PKCEDowngradeCheck] No PKCE downgrade detected.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 요청 전송 도우미 함수
|
|
||||||
private async sendRequest(sdk: SDK, req: Request, url: string, query: string) {
|
|
||||||
const spec = new RequestSpec(url);
|
|
||||||
spec.setMethod(req.getMethod());
|
|
||||||
spec.setPath(req.getPath());
|
|
||||||
spec.setQuery(query);
|
|
||||||
spec.setBody(req.getBody() as Body);
|
|
||||||
spec.setHost(req.getHost());
|
|
||||||
spec.setPort(req.getPort());
|
|
||||||
spec.setTls(req.getTls());
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(req.getHeaders())) {
|
|
||||||
spec.setHeader(key, Array.isArray(value) ? value.join(', ') : value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await sdk.requests.send(spec);
|
|
||||||
return result.response ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 경고 리포트 생성 함수
|
|
||||||
private async reportFinding(
|
|
||||||
sdk: SDK,
|
|
||||||
req: Request,
|
|
||||||
url: string,
|
|
||||||
isOpenID: boolean,
|
|
||||||
title: string,
|
|
||||||
message: string
|
|
||||||
) {
|
|
||||||
const fullTitle = isOpenID ? `[WARN] OpenID Flow ${title}` : `[WARN] OAuth2 Flow ${title}`;
|
|
||||||
await sdk.findings.create({
|
|
||||||
title: fullTitle,
|
|
||||||
description: `${message} (${url})`,
|
|
||||||
request: req,
|
|
||||||
reporter: "PKCE Checker",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,199 +0,0 @@
|
||||||
import type { Request, Response } from "caido:utils";
|
|
||||||
import type { SDK, DefineAPI } from "caido:plugin";
|
|
||||||
|
|
||||||
// 토큰 누출 검사 결과를 담는 구조
|
|
||||||
export interface TokenLeakResult {
|
|
||||||
found: boolean; // 토큰이 발견되었는지 여부 (true/false)
|
|
||||||
location: 'url' | 'body' | 'header'; // 토큰이 발견된 위치 (url, body, header 중 하나)
|
|
||||||
title: string; // 경고 제목
|
|
||||||
description: string; // 상세 설명
|
|
||||||
value?: string; // 실제 발견된 값 (선택적)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 액세스 토큰 누출 검사 클래스
|
|
||||||
export class AccessTokenLeakController {
|
|
||||||
async testReq(sdk: SDK<DefineAPI<{}>>, request: Request): Promise<void> {
|
|
||||||
const result = await this._scanRequest(request);
|
|
||||||
if (result) {
|
|
||||||
await sdk.findings.create({
|
|
||||||
title: result.title,
|
|
||||||
description: result.description,
|
|
||||||
request,
|
|
||||||
reporter: "AccessTokenLeak",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async testResp(sdk: SDK<DefineAPI<{}>>, response: Response, request: Request): Promise<void> {
|
|
||||||
const result = await this._scanResponse(response);
|
|
||||||
if (result) {
|
|
||||||
await sdk.findings.create({
|
|
||||||
title: result.title,
|
|
||||||
description: result.description,
|
|
||||||
request,
|
|
||||||
reporter: "AccessTokenLeak",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param request - 검사할 HTTP 요청 객체
|
|
||||||
* @returns 토큰이 발견되면 결과 객체, 없으면 null
|
|
||||||
*/
|
|
||||||
async _scanRequest(request: Request): Promise<TokenLeakResult | null> {
|
|
||||||
|
|
||||||
// === 1. URL에서 토큰 검사 ===
|
|
||||||
const url = request.getUrl();
|
|
||||||
|
|
||||||
const extractedTokenFromUrl = this.extractTokenFromText(url);
|
|
||||||
|
|
||||||
if (extractedTokenFromUrl) {
|
|
||||||
return {
|
|
||||||
found: true,
|
|
||||||
location: 'url',
|
|
||||||
title: "Token Leak in URL",
|
|
||||||
description: `요청 URL에 토큰이 포함되어 있습니다. (토큰: ${extractedTokenFromUrl.substring(0, 20)}...)`,
|
|
||||||
value: url
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// === 2. 요청 본문(Body)에서 토큰 검사 ===
|
|
||||||
const body = request.getBody();
|
|
||||||
|
|
||||||
if (body) {
|
|
||||||
const bodyText = await body.toText();
|
|
||||||
|
|
||||||
const extractedTokenFromBody = this.extractTokenFromText(bodyText);
|
|
||||||
|
|
||||||
if (extractedTokenFromBody) {
|
|
||||||
return {
|
|
||||||
found: true,
|
|
||||||
location: 'body',
|
|
||||||
title: "Token Leak in Request Body",
|
|
||||||
description: `요청 Body에 토큰이이 포함되어 있습니다. (토큰: ${extractedTokenFromBody.substring(0, 20)}...)`,
|
|
||||||
value: bodyText
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP 응답에서 액세스 토큰 누출 검사
|
|
||||||
* @param response - 검사할 HTTP 응답 객체
|
|
||||||
* @returns 토큰이 발견되면 결과 객체, 없으면 null
|
|
||||||
*/
|
|
||||||
async _scanResponse(response: Response): Promise<TokenLeakResult | null> {
|
|
||||||
|
|
||||||
// === 1. Location 헤더에서 토큰 검사 ===
|
|
||||||
const locationHeader = response.getHeader("Location");
|
|
||||||
|
|
||||||
const locationHeaderStr = Array.isArray(locationHeader) ? locationHeader.join(', ') : locationHeader;
|
|
||||||
|
|
||||||
if (locationHeaderStr) {
|
|
||||||
const extractedTokenFromHeader = this.extractTokenFromText(locationHeaderStr);
|
|
||||||
|
|
||||||
if (extractedTokenFromHeader) {
|
|
||||||
return {
|
|
||||||
found: true,
|
|
||||||
location: 'header',
|
|
||||||
title: "Token Leak in Redirect URL",
|
|
||||||
description: `Location 헤더에 토큰이 노출되었습니다: ${locationHeaderStr} (토큰: ${extractedTokenFromHeader.substring(0, 20)}...)`,
|
|
||||||
value: locationHeaderStr
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// === 2. 응답 본문에서 토큰 검사 ===
|
|
||||||
const bodyBytes = response.getBody();
|
|
||||||
|
|
||||||
if (bodyBytes) {
|
|
||||||
const bodyText = await bodyBytes.toText();
|
|
||||||
|
|
||||||
const extractedTokenFromBody = this.extractTokenFromText(bodyText);
|
|
||||||
|
|
||||||
if (extractedTokenFromBody) {
|
|
||||||
return {
|
|
||||||
found: true,
|
|
||||||
location: 'body',
|
|
||||||
title: "Token Leak in Response Body",
|
|
||||||
description: `HTTP 응답 본문에 토큰이 노출되었습니다. (토큰: ${extractedTokenFromBody.substring(0, 20)}...)`,
|
|
||||||
value: bodyText
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 텍스트에서 실제 토큰 값을 추출
|
|
||||||
* @param text - 검사할 텍스트
|
|
||||||
* @returns 토큰 값이 있으면 해당 값, 없으면 null
|
|
||||||
*/
|
|
||||||
private extractTokenFromText(text: string): string | null {
|
|
||||||
// 토큰 관련 키워드 리스트
|
|
||||||
const tokenKeys = [
|
|
||||||
'access_token',
|
|
||||||
'accesstoken',
|
|
||||||
'Access-Token',
|
|
||||||
'Refresh_Token',
|
|
||||||
'Refresh-Token',
|
|
||||||
'RefreshToken',
|
|
||||||
'Secret_Token',
|
|
||||||
'Secret-Token',
|
|
||||||
'SecretToken',
|
|
||||||
'SSO_Auth',
|
|
||||||
'SSO-Auth',
|
|
||||||
'SSOAuth',
|
|
||||||
'auth_token',
|
|
||||||
'session_token'
|
|
||||||
];
|
|
||||||
|
|
||||||
const tokenTypeKeys = [
|
|
||||||
'token_type',
|
|
||||||
'tokenType'
|
|
||||||
];
|
|
||||||
|
|
||||||
// 정규표현식 토큰 타입 유무 패턴 리스트 생성
|
|
||||||
const tokenTypeRegexes: RegExp[] = [];
|
|
||||||
for (const key of tokenTypeKeys) {
|
|
||||||
// JSON 형식: "token_type": "Bearer"
|
|
||||||
tokenTypeRegexes.push(new RegExp(`"${key}"\\s*:\\s*"bearer"`, 'i'));
|
|
||||||
// 일반 key=value 형식: token_type=Bearer
|
|
||||||
tokenTypeRegexes.push(new RegExp(`${key}[=:]\\s*bearer`, 'i'));
|
|
||||||
// 공백 있는 형식: token_type : Bearer
|
|
||||||
tokenTypeRegexes.push(new RegExp(`${key}\\s*:\\s*bearer`, 'i'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// token_type=bearer 형태 중 하나라도 포함되는지 확인
|
|
||||||
const hasTokenTypeBearer = tokenTypeRegexes.some(rx => rx.test(text));
|
|
||||||
|
|
||||||
// 정규표현식 토큰 유무 패턴 리스트 생성
|
|
||||||
const tokenPatterns: RegExp[] = [];
|
|
||||||
|
|
||||||
for (const key of tokenKeys) {
|
|
||||||
// 1. key=token 또는 key: token
|
|
||||||
tokenPatterns.push(new RegExp(`${key}[=:]\\s*([a-zA-Z0-9\\-._~+/]+=*)`, 'i'));
|
|
||||||
|
|
||||||
// 2. JSON 형태의 "key": "token"
|
|
||||||
tokenPatterns.push(new RegExp(`"${key}"\\s*:\\s*"([^"]+)"`, 'i'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Authorization: Bearer <token> 형태
|
|
||||||
tokenPatterns.push(/bearer\s+([a-zA-Z0-9\-._~+/]+=*)/i);
|
|
||||||
|
|
||||||
// 모든 패턴에 대해 검사
|
|
||||||
for (const pattern of tokenPatterns) {
|
|
||||||
const match = pattern.exec(text);
|
|
||||||
if (match && match[1]) {
|
|
||||||
if(hasTokenTypeBearer){
|
|
||||||
return match[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,277 +0,0 @@
|
||||||
import type { Request, Response } from "caido:utils";
|
|
||||||
import type { SDK, DefineAPI } from "caido:plugin";
|
|
||||||
import { HttpUtils } from "../utils/http";
|
|
||||||
|
|
||||||
const httpUtils = new HttpUtils();
|
|
||||||
|
|
||||||
export class CsrfCheck {
|
|
||||||
private nonceParam = [
|
|
||||||
"state",
|
|
||||||
"nonce",
|
|
||||||
"as",
|
|
||||||
"frame_id",
|
|
||||||
"csrf_token",
|
|
||||||
"csrf",
|
|
||||||
];
|
|
||||||
|
|
||||||
private isTargetUri(uri: string): boolean {
|
|
||||||
if (
|
|
||||||
httpUtils.getQueryParamFromURI(uri, "client_id") !== null &&
|
|
||||||
(httpUtils.getQueryParamFromURI(uri, "response_type") !== null ||
|
|
||||||
httpUtils.getQueryParamFromURI(uri, "grant_type") !== null ||
|
|
||||||
httpUtils.getQueryParamFromURI(uri, "redirect_uri") !== null ||
|
|
||||||
httpUtils.getQueryParamFromURI(uri, "scope") !== null ||
|
|
||||||
httpUtils.getQueryParamFromURI(uri, "state") !== null ||
|
|
||||||
httpUtils.getQueryParamFromURI(uri, "nonce") !== null)
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private isOauthUri(request: Request): boolean {
|
|
||||||
const uri = request.getUrl() || "";
|
|
||||||
|
|
||||||
// Check if the request is an OAuth authorization request
|
|
||||||
if (this.isTargetUri(uri)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private isOauthRedirectResponse(response: Response): boolean {
|
|
||||||
const status = response.getCode();
|
|
||||||
const uri =
|
|
||||||
httpUtils.getHeaderValue(response.getHeaders(), "location") || "";
|
|
||||||
|
|
||||||
if (status >= 300 && status < 400 && this.isTargetUri(uri)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private isNonceInQuery(request: Request): boolean {
|
|
||||||
const query = request.getQuery() || "";
|
|
||||||
|
|
||||||
for (const param of this.nonceParam) {
|
|
||||||
if (httpUtils.getQueryParam(query, param) !== null) {
|
|
||||||
return true; // Nonce parameter is present in the query
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false; // No nonce parameter found in the query
|
|
||||||
}
|
|
||||||
|
|
||||||
private getNonceParamName(url: string): string | null {
|
|
||||||
for (const param of this.nonceParam) {
|
|
||||||
if (httpUtils.getQueryParamFromURI(url, param) !== null) {
|
|
||||||
return param; // Return the first matching nonce parameter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null; // No nonce parameter found
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkNonceAtResponseLocationHeader(
|
|
||||||
request: Request,
|
|
||||||
response: Response
|
|
||||||
): string[] | 0 {
|
|
||||||
const nonceParamName = this.getNonceParamName(request.getUrl() || "");
|
|
||||||
|
|
||||||
if (
|
|
||||||
!this.isOauthUri(request) ||
|
|
||||||
!this.isNonceInQuery(request) ||
|
|
||||||
!this.isOauthRedirectResponse(response) ||
|
|
||||||
!nonceParamName
|
|
||||||
) {
|
|
||||||
return 0; // Not a target, no CSRF risk
|
|
||||||
}
|
|
||||||
|
|
||||||
// 요청에서 보낸 Nonce 추출
|
|
||||||
const query = request.getQuery() || "";
|
|
||||||
const originalNonce = httpUtils.getQueryParam(query, nonceParamName);
|
|
||||||
|
|
||||||
// 리다이렉트 URL에서 쿼리 부분만 추출
|
|
||||||
const locationHeader =
|
|
||||||
httpUtils.getHeaderValue(response.getHeaders(), "location") || "";
|
|
||||||
|
|
||||||
const responseNonce = httpUtils.getQueryParamFromURI(
|
|
||||||
locationHeader || "",
|
|
||||||
nonceParamName
|
|
||||||
);
|
|
||||||
|
|
||||||
// Nonce가 없거나, 요청값과 다르면 CSRF 위험
|
|
||||||
if (!responseNonce) {
|
|
||||||
// missing state
|
|
||||||
return ["Nonce parameter is missing in the response location header"];
|
|
||||||
}
|
|
||||||
if (originalNonce !== responseNonce) {
|
|
||||||
// mismatch
|
|
||||||
return ["Nonce parameter mismatch between request and response"];
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0; // no CSRF risk detected
|
|
||||||
}
|
|
||||||
|
|
||||||
private async checkNonceReuse(
|
|
||||||
sdk: SDK<DefineAPI<{}>, {}>,
|
|
||||||
request: Request,
|
|
||||||
originResponse: Response
|
|
||||||
): Promise<string[] | 0> {
|
|
||||||
// uri에 oauth 관련 파라미터가 없지만, 응답이 oauth 리다이렉트 응답인지 확인
|
|
||||||
// 즉, 처음으로 Nonce를 발급한 요청인지 확인
|
|
||||||
if (
|
|
||||||
this.isOauthUri(request) ||
|
|
||||||
!this.isOauthRedirectResponse(originResponse)
|
|
||||||
) {
|
|
||||||
return 0; // Not a target, no CSRF risk
|
|
||||||
}
|
|
||||||
|
|
||||||
// 기존 응답의 location 헤더의 url에서 Nonce 파라미터 이름, nonce 파라미터 값, 쿼리 추출
|
|
||||||
const originResponseLocationHeader =
|
|
||||||
httpUtils.getHeaderValue(originResponse.getHeaders(), "location") || "";
|
|
||||||
const nonceParamName =
|
|
||||||
this.getNonceParamName(originResponseLocationHeader || "") || "state";
|
|
||||||
const originLocationQuery =
|
|
||||||
httpUtils.getQueryFromURI(originResponseLocationHeader || "") || "";
|
|
||||||
const originLocationNonce = httpUtils.getQueryParam(
|
|
||||||
originLocationQuery,
|
|
||||||
nonceParamName
|
|
||||||
);
|
|
||||||
|
|
||||||
// 쿠키가 없는 헤더로 새로운 nonce를 발급받기 위해 요청
|
|
||||||
const noCookieHeaders = httpUtils.removeHeaders(request.getHeaders(), [
|
|
||||||
"cookie",
|
|
||||||
]);
|
|
||||||
const noCookieResponse = await httpUtils.resend(sdk, request, {
|
|
||||||
headers: noCookieHeaders,
|
|
||||||
});
|
|
||||||
if (!noCookieResponse || noCookieResponse?.getCode() >= 400) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 쿠키가 없는 응답의 location 헤더 추출 및 Nonce 추출
|
|
||||||
const noCookieLocationHeader = httpUtils.getHeaderValue(
|
|
||||||
noCookieResponse?.getHeaders() || {},
|
|
||||||
"location"
|
|
||||||
);
|
|
||||||
const newNonce =
|
|
||||||
httpUtils.getQueryParamFromURI(
|
|
||||||
noCookieLocationHeader || "",
|
|
||||||
nonceParamName
|
|
||||||
) || "";
|
|
||||||
|
|
||||||
if (originLocationNonce === newNonce) {
|
|
||||||
return [
|
|
||||||
"State parameter reused in the response location header, indicating a potential CSRF risk",
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 기존 쿠키와 함께 새로운 Nonce로 요청
|
|
||||||
const newQuery = httpUtils.setQueryParam(
|
|
||||||
originLocationQuery,
|
|
||||||
nonceParamName,
|
|
||||||
newNonce
|
|
||||||
);
|
|
||||||
|
|
||||||
// 기존 location 헤더의 uri 요청과 location 헤더에서 nonce값만 새로 발급한 값으로 바꾸어 요청한 결과를 비교
|
|
||||||
const res1 = await httpUtils.customFetch(
|
|
||||||
sdk,
|
|
||||||
originResponseLocationHeader,
|
|
||||||
"GET",
|
|
||||||
originLocationQuery,
|
|
||||||
request.getHeaders()
|
|
||||||
);
|
|
||||||
|
|
||||||
const res2 = await httpUtils.customFetch(
|
|
||||||
sdk,
|
|
||||||
originResponseLocationHeader,
|
|
||||||
"GET",
|
|
||||||
newQuery,
|
|
||||||
request.getHeaders()
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
!res1 ||
|
|
||||||
!res2 ||
|
|
||||||
res1.getCode() >= 400 ||
|
|
||||||
res2.getCode() >= 400 ||
|
|
||||||
res1.getCode() !== res2.getCode()
|
|
||||||
) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
res1.getCode() === res2.getCode() &&
|
|
||||||
300 <= res1.getCode() &&
|
|
||||||
res1.getCode() < 400
|
|
||||||
) {
|
|
||||||
const res1LocationHeader =
|
|
||||||
httpUtils.getHeaderValue(res1.getHeaders(), "location") || "";
|
|
||||||
const res2LocationHeader =
|
|
||||||
httpUtils.getHeaderValue(res2.getHeaders(), "location") || "";
|
|
||||||
const res1ReirectPath = httpUtils.getPathFromURI(res1LocationHeader);
|
|
||||||
const res2ReirectPath = httpUtils.getPathFromURI(res2LocationHeader);
|
|
||||||
|
|
||||||
if (res1ReirectPath === res2ReirectPath) {
|
|
||||||
return [
|
|
||||||
"When nonce parameter reused in the response location header, it might not be verified. Indicating a potential CSRF risk",
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0; // no CSRF risk detected
|
|
||||||
}
|
|
||||||
|
|
||||||
async checker(
|
|
||||||
sdk: SDK<DefineAPI<{}>, {}>,
|
|
||||||
request: Request,
|
|
||||||
response: Response
|
|
||||||
): Promise<void> {
|
|
||||||
let result = ``;
|
|
||||||
|
|
||||||
// 쿼리에 state 파라미터가 없으면 CSRF 위험
|
|
||||||
try {
|
|
||||||
if (this.isOauthUri(request) && !this.isNonceInQuery(request)) {
|
|
||||||
result += "CSRF risk: missing state parameter"; // CSRF risk: missing state parameter
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
sdk.console.error(`Error checking state in query: ${error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// location 헤더에 state 파라미터가 없거나, 요청에서 보낸 state와 다르면 CSRF 위험
|
|
||||||
try {
|
|
||||||
const stateAtResponseLocationHeaderCheck =
|
|
||||||
this.checkNonceAtResponseLocationHeader(request, response);
|
|
||||||
if (stateAtResponseLocationHeaderCheck !== 0) {
|
|
||||||
result += `, ${stateAtResponseLocationHeaderCheck.join(", ")}`;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
sdk.console.error(
|
|
||||||
`Error checking state in response location header: ${error}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 처음으로 state를 발급한 요청에서 state 파라미터를 바꿔서 보내기
|
|
||||||
const reusedStateCheck = await this.checkNonceReuse(sdk, request, response);
|
|
||||||
if (reusedStateCheck !== 0) {
|
|
||||||
result += `, ${reusedStateCheck.join(", ")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
result.replace(/^\s*,\s*|\s*$/, ""); // Remove leading/trailing commas
|
|
||||||
if (result) {
|
|
||||||
await sdk.findings.create({
|
|
||||||
title: "csrf vuln",
|
|
||||||
description: `SSO-related parameters detected in response:\n\n${request.getMethod()} ${request.getUrl()} : ${result}`,
|
|
||||||
request,
|
|
||||||
reporter: "csrf reporter",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
sdk.console.error(`Error creating finding: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Request, Response } from "caido:utils";
|
import type { Request } from "caido:utils";
|
||||||
import { TokenLeakCheck } from "./tokenLeakCheck";
|
import { TokenLeakCheck } from "./tokenLeakCheck";
|
||||||
|
|
||||||
export class NonceCheckController{
|
export class NonceCheckController{
|
||||||
|
|
@ -6,8 +6,8 @@ export class NonceCheckController{
|
||||||
* 응답이 OIDC(OpenID Connect) 플로우인지 확인하는 메서드
|
* 응답이 OIDC(OpenID Connect) 플로우인지 확인하는 메서드
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public static isOidcFlow(req: Request, res:Response): boolean {
|
public static isOidcFlow(req: Request): boolean {
|
||||||
if(TokenLeakCheck.extractIdToken(req, res)) {
|
if(TokenLeakCheck.extractIdToken(req)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -15,10 +15,10 @@ export class NonceCheckController{
|
||||||
|
|
||||||
|
|
||||||
public static isNonceCheckRequest(req: Request): boolean {
|
public static isNonceCheckRequest(req: Request): boolean {
|
||||||
const id_token = TokenLeakCheck.decodeIdToken(req);
|
const id_token = decodeIdToken(req);
|
||||||
|
|
||||||
// 1. nonce 파라미터가 포함된 요청인지 확인
|
// 1. nonce 파라미터가 포함된 요청인지 확인
|
||||||
if (id_token && id_token.includes("nonce=")) {
|
if (id_token.includes("nonce=")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -26,4 +26,8 @@ export class NonceCheckController{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function decodeIdToken(req: Request): string {
|
||||||
|
// Implement actual decoding logic here. For now, return an empty string or mock value.
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
95
packages/backend/src/controller/redirectUriCheck.ts
Normal file
95
packages/backend/src/controller/redirectUriCheck.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
// oauth-plugin.ts
|
||||||
|
import type { SDK } from "caido:plugin";
|
||||||
|
import type { Request, Response } from "caido:utils";
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
import * as path from "path";
|
||||||
|
import os from "os";
|
||||||
|
|
||||||
|
export class redirectUriCheck {
|
||||||
|
private requestMap = new Map<string, string>();
|
||||||
|
|
||||||
|
// constructor(private sdk: SDK<any>) {}
|
||||||
|
|
||||||
|
public async onRequest(sdk: SDK, req: Request) {
|
||||||
|
try {
|
||||||
|
const urlString = req.getUrl();
|
||||||
|
const url = new URL(urlString);
|
||||||
|
sdk.console.log(`[OAuthPlugin] Intercepted request: ${urlString}`);
|
||||||
|
|
||||||
|
if (!url.pathname.includes("/authorize") && !url.pathname.includes("/auth")) return;
|
||||||
|
|
||||||
|
const params = new URLSearchParams(url.search);
|
||||||
|
const redirectUri = params.get("redirect_uri");
|
||||||
|
if (!redirectUri) return;
|
||||||
|
|
||||||
|
const reqId = req.getId();
|
||||||
|
this.requestMap.set(reqId, redirectUri);
|
||||||
|
|
||||||
|
const clientId = params.get("client_id") ?? "(missing)";
|
||||||
|
const responseType = params.get("response_type") ?? "(missing)";
|
||||||
|
const isScan = params.has("scan");
|
||||||
|
if (isScan) return;
|
||||||
|
|
||||||
|
const output = {
|
||||||
|
original_url: urlString,
|
||||||
|
client_id: clientId,
|
||||||
|
redirect_uri: redirectUri,
|
||||||
|
response_type: responseType,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const filePath = path.join(os.tmpdir(), "oauth-fuzz-input.json");
|
||||||
|
await fs.writeFile(filePath, JSON.stringify(output, null, 2));
|
||||||
|
} catch (err) {
|
||||||
|
await sdk.findings.create({
|
||||||
|
title: "[fs] Write Failed",
|
||||||
|
description: `Could not write to file: ${err}`,
|
||||||
|
request: req,
|
||||||
|
reporter: "oauth-open-redirect-detector"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await sdk.findings.create({
|
||||||
|
title: "[ ] OAuth2 Authorization Request Collected",
|
||||||
|
description: `client_id: ${clientId}\nredirect_uri: ${redirectUri}\nresponse_type: ${responseType}`,
|
||||||
|
request: req,
|
||||||
|
reporter: "oauth-open-redirect-detector"
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
sdk.console.error(`Error in onRequest: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async onResponse(sdk: SDK, req: Request, resp: Response) {
|
||||||
|
try {
|
||||||
|
const reqId = req.getId();
|
||||||
|
const url = new URL(req.getUrl());
|
||||||
|
const status = resp.getCode();
|
||||||
|
const location = resp.getHeader("location")?.[0];
|
||||||
|
|
||||||
|
const params = new URLSearchParams(url.search);
|
||||||
|
const isScan = params.has("scan");
|
||||||
|
|
||||||
|
if (!isScan) {
|
||||||
|
this.requestMap.delete(reqId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status >= 300 && status < 400 && location) {
|
||||||
|
const redirectUri = this.requestMap.get(reqId) ?? "(unknown)";
|
||||||
|
|
||||||
|
await sdk.findings.create({
|
||||||
|
title: "[+] Redirect URI Misconfiguration Detected",
|
||||||
|
description: `Status: ${status}\nLocation: ${location}\nRequest URL: ${url.href}\nRedirect URI: ${redirectUri}`,
|
||||||
|
request: req,
|
||||||
|
reporter: "oauth-open-redirect-detector",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.requestMap.delete(reqId);
|
||||||
|
} catch (err) {
|
||||||
|
sdk.console.error(`Error in onResponse: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
import type { Request, Response } from "caido:utils";
|
|
||||||
import type { SDK } from "caido:plugin";
|
|
||||||
|
|
||||||
export class RedirectBypassController {
|
|
||||||
// redirect_uri를 확인하는 함수
|
|
||||||
isRedirectUri(req: Request): { detected: boolean; redirectUri?: string } {
|
|
||||||
// ? 뒤에 오는 파라미터 모두 가져오고, 정규표현식으로 redirect_uri= 이후 주소만 뽑음(없으면 null)
|
|
||||||
const query = req.getQuery();
|
|
||||||
const redirectUriMatch = query.match(/redirect_uri=([^&]+)/i);
|
|
||||||
|
|
||||||
// redirectUriMatch[1]은 ()로 감싼 부분
|
|
||||||
// redirect_uri 파라미터가 없거나 있어도 주소가 문자열이 아니면 false
|
|
||||||
if (!redirectUriMatch || typeof redirectUriMatch[1] !== "string") {
|
|
||||||
return { detected: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 인코딩된 주소를 원래대로 바꿈 (ex. https://~~)
|
|
||||||
const redirectUri = decodeURIComponent(redirectUriMatch[1]);
|
|
||||||
|
|
||||||
const bypassPatterns = [
|
|
||||||
"%ff@", "/", "%2f@", "%0a@", "%0d@", "\\", ".evil.com", "@", "%2f..%2f",
|
|
||||||
];
|
|
||||||
|
|
||||||
// 위 패턴에 일치하는 게 있으면 true랑 redirectUri 반환 (false일 땐 undefined)
|
|
||||||
const detected = bypassPatterns.some(pattern => redirectUri.includes(pattern));
|
|
||||||
return { detected, redirectUri: detected ? redirectUri : undefined };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 응답에 인가 코드가 포함되어 있는지 확인하는 함수
|
|
||||||
isCodeIssued(res: Response): boolean {
|
|
||||||
const location = res.getHeader("Location") || "";
|
|
||||||
return location.includes("code=");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 위의 두 함수 모두 만족하면 true, 문제의 주소를 반환하는 함수
|
|
||||||
test(req: Request, res: Response): { detected: boolean; redirectUri?: string } {
|
|
||||||
const redirectCheck = this.isRedirectUri(req);
|
|
||||||
const codeIssued = this.isCodeIssued(res);
|
|
||||||
|
|
||||||
if (redirectCheck.detected && codeIssued) {
|
|
||||||
return { detected: true, redirectUri: redirectCheck.redirectUri };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { detected: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 탐지된 결과 저장하는 함수
|
|
||||||
async testAsync(sdk: SDK, req: Request, res: Response): Promise<void> {
|
|
||||||
const result = this.test(req, res);
|
|
||||||
if (result.detected) {
|
|
||||||
await sdk.findings.create({
|
|
||||||
title: "Redirect URI Bypass Detected",
|
|
||||||
description: `redirect_uri 우회 발견\nRedirect URI: ${result.redirectUri}`,
|
|
||||||
request: req,
|
|
||||||
reporter: "gyu",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
import type { SDK } from "caido:plugin";
|
|
||||||
import { RequestSpec } from "caido:utils";
|
|
||||||
|
|
||||||
export class ScopeDetection {
|
|
||||||
async scan(
|
|
||||||
sdk: SDK,
|
|
||||||
url: string
|
|
||||||
): Promise<{ data: string }> {
|
|
||||||
sdk.console.log(`들어온 url : ${url}`); // url이 잘 들어왔는지 확인함요
|
|
||||||
|
|
||||||
// url이 string이 아니고 , 값이 없거나 그럴 때 유효한 값 넣으라고 출력.
|
|
||||||
if (!url || typeof url !== "string") {
|
|
||||||
sdk.console.log("이상한 url 입력함.");
|
|
||||||
return { data: "알맞은 URL을 입력하세요." };
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const spec = new RequestSpec(url); // url에 GET 요청 보낼거긔.
|
|
||||||
spec.setMethod("GET");
|
|
||||||
spec.setHeader("User-Agent", "Caido Scanner");
|
|
||||||
spec.setHeader("Accept", "*/*");
|
|
||||||
sdk.console.log(`요청 URL: ${url}`);
|
|
||||||
|
|
||||||
const res = await sdk.requests.send(spec); // 요청 보내고 응답 받음.
|
|
||||||
sdk.console.log('[SCAN] 응답 :', res);
|
|
||||||
sdk.console.log(`[SCAN] 요청 성공:${(res as any).status}`);
|
|
||||||
sdk.console.log(`[SCAN] body: ${(res as any).body ? (res as any).body.toString().substring(0, 100) : "없음"}`);
|
|
||||||
|
|
||||||
const html = (res as any).body ? (res as any).body.toString() : "";
|
|
||||||
|
|
||||||
// <a href= 형태 링크 찾을거임.
|
|
||||||
const anchorRegex = /<a\s+[^>]*href="([^"]+)"[^>]*>/gi;
|
|
||||||
const anchors: string[] = [];
|
|
||||||
let match;
|
|
||||||
while ((match = anchorRegex.exec(html)) !== null) { // html에서 a href 찾아 배열에 저장함.
|
|
||||||
if (typeof match[1] === "string") {
|
|
||||||
anchors.push(match[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sdk.console.log(`찾아진 a href 개수: ${anchors.length}`);
|
|
||||||
|
|
||||||
// 5. scope 탐지
|
|
||||||
const results: string[] = [];
|
|
||||||
anchors.forEach((href) => { // 추출한 a href 링크 하나씩 검사드감.
|
|
||||||
try {
|
|
||||||
const absHref = new URL(href, url).href; // 상대경로라면 url 기준으로 절대 URL 바꿔줌줌
|
|
||||||
sdk.console.log(`[SCAN] 절대 URL 변환: ${href} -> ${absHref}`); //
|
|
||||||
|
|
||||||
if (/oauth|authorize|login|accounts|auth/i.test(absHref)) { // url에 이런 OAuth 키워드가 있는지 필터링.
|
|
||||||
let u: URL;
|
|
||||||
try {
|
|
||||||
u = new URL(absHref); // 필터링된 url을 url 객체로 파싱. 정식 url인 경우 변수 u에 저장.
|
|
||||||
} catch (err) { // 파싱 실패하면
|
|
||||||
sdk.console.log(
|
|
||||||
`URL 파싱 실패 : ${absHref} (${err instanceof Error ? err.message : err})`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const scope = u.searchParams.get("scope"); // url에 scope있긔?scope값 가져와.
|
|
||||||
if (scope && /all|\*/i.test(scope)) { // scope가 존재하고 all, *있다면.
|
|
||||||
results.push(`위험한 scope 발견: ${scope}\n -> ${absHref}`); // results에 경고 메시지 전달.
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
sdk.console.log(`searchParams.get 실패`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
sdk.console.log(
|
|
||||||
`URL 파싱 실패 (absHref 단계): ${href} (${e instanceof Error ? e.message : e})`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const resultStr = results.join("\n") || "위험한 scope가 발견되지 않았습니다.";
|
|
||||||
return { data: resultStr }; // 성공했는지 실패했는지 App.vue한테 전달할 메시지.
|
|
||||||
} catch (e) {
|
|
||||||
sdk.console.log(`백엔드 에러: ${e instanceof Error ? e.message : e}`);
|
|
||||||
return { data: "백엔드 에러: " + (e instanceof Error ? e.message : String(e)) }; // App.vue에 전달할 메시지.
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import type { Request,Response } from "caido:utils";
|
import type { Request } from "caido:utils";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
export class TokenLeakCheck {
|
export class TokenLeakCheck {
|
||||||
public static extractIdToken(req: Request, res?: Response): string | null {
|
public static extractIdToken(req: Request): string | null {
|
||||||
// 1. Authorization 헤더 확인\\
|
// 1. Authorization 헤더 확인\\
|
||||||
const header = req.getHeaders() as Record<string, string | string[] | undefined>;
|
const header = req.getHeaders() as Record<string, string | string[] | undefined>;
|
||||||
const authHeader = header["authorization"] || header["Authorization"];
|
const authHeader = header["authorization"] || header["Authorization"];
|
||||||
|
|
@ -16,21 +16,19 @@ export class TokenLeakCheck {
|
||||||
return (query as Record<string, any>).id_token;
|
return (query as Record<string, any>).id_token;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. response 안에 id_token이 있을 경우
|
// 3. POST 바디 안에 id_token이 있을 경우
|
||||||
if (res) {
|
const rawBody = req.getRaw();
|
||||||
const rawBody = res.getRaw();
|
const body = rawBody ? rawBody.toString() : "";
|
||||||
const body = rawBody ? rawBody.toString() : "";
|
const match = body.match(/id_token=([^&\s]+)/);
|
||||||
const match = body.match(/id_token=([^&\s]+)/);
|
if (match && typeof match[1] === "string") {
|
||||||
if (match && typeof match[1] === "string" ) {
|
return decodeURIComponent(match[1]);
|
||||||
return decodeURIComponent(match[1]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static decodeIdToken(req: Request, res?: Response): Record<string, any> | null {
|
public static decodeIdToken(req: Request): Record<string, any> | null {
|
||||||
const token = this.extractIdToken(req, res);
|
const token = this.extractIdToken(req);
|
||||||
if (!token) return null;
|
if (!token) return null;
|
||||||
|
|
||||||
const decoded = jwt.decode(token, { complete: true });
|
const decoded = jwt.decode(token, { complete: true });
|
||||||
|
|
|
||||||
|
|
@ -2,60 +2,54 @@ import type { SDK, DefineAPI } from "caido:plugin";
|
||||||
import type { Request, Response } from "caido:utils";
|
import type { Request, Response } from "caido:utils";
|
||||||
// import { ImplicitGrantController } from "./controller/implictGrant";
|
// import { ImplicitGrantController } from "./controller/implictGrant";
|
||||||
// import { AuthZCodeGrantController } from "./controller/authZCodeGrant";
|
// import { AuthZCodeGrantController } from "./controller/authZCodeGrant";
|
||||||
import { CsrfCheck } from "./controller/csrfCheck";
|
|
||||||
import { PKCECheck } from "./controller/PKCECheck";
|
import { PKCECheck } from "./controller/PKCECheck";
|
||||||
import { AccessTokenLeakController } from "./controller/accessTokenDetector";
|
import { redirectUriCheck } from "./controller/redirectUriCheck";
|
||||||
import { ScopeDetection } from "./controller/scopeDetection";
|
|
||||||
// import { NonceCheckController } from "./controller/nonceCheck";
|
|
||||||
import { RedirectBypassController } from "./controller/redirect_uriBypass";
|
|
||||||
|
|
||||||
export type API = DefineAPI<{}>;
|
export type API = DefineAPI<{}>;
|
||||||
|
|
||||||
const csrfCheck = new CsrfCheck();
|
// const implicitGrantController = new ImplicitGrantController();
|
||||||
|
// const authZCodeGrantController = new AuthZCodeGrantController();
|
||||||
const pkceCheckController = new PKCECheck();
|
const pkceCheckController = new PKCECheck();
|
||||||
const tokenCheck = new AccessTokenLeakController();
|
const redirectUriCheckController = new redirectUriCheck();
|
||||||
const ScopeDetectionController = new ScopeDetection();
|
|
||||||
// const nonceCheckController = new NonceCheckController();
|
|
||||||
const redirectBypassController = new RedirectBypassController();
|
|
||||||
|
|
||||||
export function init(sdk: SDK<API>) {
|
export function init(sdk: SDK<API>) {
|
||||||
sdk.events.onInterceptResponse(async (sdk, req: Request, res: Response) => {
|
// sdk.events.onInterceptRequest(async (sdk, req: Request) => {
|
||||||
await csrfCheck.checker(sdk, req, res);
|
// const result = csrfCheck.checker(req);
|
||||||
//await pkceCheckController.test(sdk, req);
|
|
||||||
await tokenCheck.testResp(sdk, res, req);
|
|
||||||
await ScopeDetectionController.scan(sdk, req.getUrl());
|
|
||||||
await redirectBypassController.testAsync(sdk, req, res);
|
|
||||||
|
|
||||||
// if (NonceCheckController.isOidcFlow(req, res)) {
|
// if (result) {
|
||||||
// await sdk.findings.create({
|
// await sdk.findings.create({
|
||||||
// title: "OIDC Flow Detected",
|
// title: "Possible SSO Request Detected",
|
||||||
// description: "The request appears to be part of an OIDC flow.",
|
// description: `SSO-related parameters detected in request:\n\n${req.getMethod()} ${req.getUrl()} : ${result}`,
|
||||||
// request: req,
|
// request: req,
|
||||||
// reporter: "",
|
// reporter: "",
|
||||||
// });
|
// });
|
||||||
// }
|
// }
|
||||||
});
|
// });
|
||||||
|
sdk.events.onInterceptRequest(
|
||||||
sdk.events.onInterceptRequest(async (sdk, req: Request) => {
|
async(sdk: SDK<DefineAPI<{}>, {}>, req: Request) => {
|
||||||
await tokenCheck.testReq(sdk, req);
|
await redirectUriCheckController.onRequest(sdk, req);
|
||||||
await pkceCheckController.test(sdk, req);
|
|
||||||
});
|
|
||||||
/*
|
|
||||||
sdk.events.onInterceptRequest(async (sdk, req: Request) => {
|
|
||||||
const result =
|
|
||||||
authZCodeGrantController.testReq(req) ||
|
|
||||||
implicitGrantController.testReq(req);
|
|
||||||
|
|
||||||
if (result) {
|
|
||||||
await pkceCheckController.test(sdk, req);
|
|
||||||
|
|
||||||
await sdk.findings.create({
|
|
||||||
title: "Possible SSO Request Detected",
|
|
||||||
description: `SSO-related parameters detected in request:\n\n${req.getMethod()} ${req.getUrl()} : ${result}`,
|
|
||||||
request: req,
|
|
||||||
reporter: "",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
*/
|
|
||||||
}
|
sdk.events.onInterceptResponse(
|
||||||
|
async (sdk: SDK<DefineAPI<{}>, {}>, req: Request, resp: Response) => {
|
||||||
|
await pkceCheckController.test(sdk, req);
|
||||||
|
await redirectUriCheckController.onResponse(sdk, req, resp);
|
||||||
|
|
||||||
|
// sdk.events.onInterceptRequest(async (sdk, req: Request) => {
|
||||||
|
// const result =
|
||||||
|
// authZCodeGrantController.testReq(req) ||
|
||||||
|
// implicitGrantController.testReq(req);
|
||||||
|
|
||||||
|
// if (result) {
|
||||||
|
// await pkceCheckController.test(sdk, req);
|
||||||
|
|
||||||
|
// await sdk.findings.create({
|
||||||
|
// title: "Possible SSO Request Detected",
|
||||||
|
// description: `SSO-related parameters detected in request:\n\n${req.getMethod()} ${req.getUrl()} : ${result}`,
|
||||||
|
// request: req,
|
||||||
|
// reporter: "",
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,331 +0,0 @@
|
||||||
import type { SDK } from "caido:plugin";
|
|
||||||
import { Body, RequestSpec, type Request, type Response } from "caido:utils";
|
|
||||||
|
|
||||||
let instance: HttpUtils | null = null;
|
|
||||||
export class HttpUtils {
|
|
||||||
/**
|
|
||||||
* 싱글턴 인스턴스를 생성합니다.
|
|
||||||
*/
|
|
||||||
public constructor() {
|
|
||||||
if (instance) {
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
instance = this;
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
encodeAndLower(value: string): string {
|
|
||||||
try {
|
|
||||||
return encodeURIComponent(value).toLowerCase();
|
|
||||||
} catch {
|
|
||||||
return value.toLowerCase();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* URI 디코딩 후 소문자로 변환하는 헬퍼 함수
|
|
||||||
* @param value - 디코딩하고 소문자로 변환할 문자열
|
|
||||||
* @returns 디코딩 및 소문자 변환된 문자열
|
|
||||||
*/
|
|
||||||
decodeAndLower(value: string): string {
|
|
||||||
try {
|
|
||||||
return decodeURIComponent(value).toLowerCase();
|
|
||||||
} catch {
|
|
||||||
return value.toLowerCase();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 헤더 객체의 키와 값을 전부 소문자로 변환합니다.
|
|
||||||
* @param headers - Record<string, string | string[]> 형태의 헤더 맵
|
|
||||||
* @returns - 키와 값이 모두 소문자로 변환된 새 헤더 맵
|
|
||||||
*/
|
|
||||||
lowerCaseAllHeaders(
|
|
||||||
headers: Record<string, string | string[]>
|
|
||||||
): Record<string, string | string[]> {
|
|
||||||
const result: Record<string, string | string[]> = {};
|
|
||||||
|
|
||||||
for (const [rawKey, rawValue] of Object.entries(headers)) {
|
|
||||||
const key = this.decodeAndLower(rawKey);
|
|
||||||
|
|
||||||
if (Array.isArray(rawValue)) {
|
|
||||||
result[key] = rawValue.map((v) => this.decodeAndLower(v));
|
|
||||||
} else {
|
|
||||||
result[key] = this.decodeAndLower(rawValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
getPathFromURI(uri: string): string | null {
|
|
||||||
uri = uri.toLowerCase();
|
|
||||||
try {
|
|
||||||
const urlObj = new URL(uri);
|
|
||||||
const path = urlObj.pathname;
|
|
||||||
return path ? decodeURIComponent(path) : null; // 경로가 없으면 null 반환
|
|
||||||
} catch (e) {
|
|
||||||
return null; // URL 파싱 실패 시 null 반환
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getQueryFromURI(uri: string): string | null {
|
|
||||||
uri = uri.toLowerCase();
|
|
||||||
try {
|
|
||||||
const urlObj = new URL(uri);
|
|
||||||
const query = urlObj.search;
|
|
||||||
return query ? decodeURIComponent(query.slice(1)) : null; // 쿼리 문자열에서 ? 제거
|
|
||||||
} catch (e) {
|
|
||||||
return null; // URL 파싱 실패 시 null 반환
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getQueryParamFromURI(uri: string, key: string): string | null {
|
|
||||||
uri = uri.toLowerCase();
|
|
||||||
key = this.decodeAndLower(key);
|
|
||||||
try {
|
|
||||||
const urlObj = new URL(uri);
|
|
||||||
const param = urlObj.searchParams.get(key);
|
|
||||||
return param ? decodeURIComponent(param) : null;
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query
|
|
||||||
/**
|
|
||||||
* 주어진 쿼리 문자열(query)에서 key에 해당하는 값을 반환합니다.
|
|
||||||
* @param query - "a=1&b=2..." 형태의 쿼리 문자열 (맨 앞 ? 는 없어야 합니다)
|
|
||||||
* @param key - 가져오고 싶은 파라미터 이름
|
|
||||||
* @returns - 해당 파라미터 값, 없으면 null
|
|
||||||
*/
|
|
||||||
getQueryParam(query: string, key: string): string | null {
|
|
||||||
query = query.toLowerCase();
|
|
||||||
key = this.decodeAndLower(key);
|
|
||||||
|
|
||||||
const params = new URLSearchParams(query);
|
|
||||||
const targetParam = params.get(key);
|
|
||||||
return targetParam ? decodeURIComponent(targetParam) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 주어진 쿼리 문자열(query)에 key=value를 설정하고, 전체 쿼리 문자열을 반환합니다.
|
|
||||||
* - 이미 key가 있으면 덮어쓰기(set), 없으면 새로 추가합니다.
|
|
||||||
* @param query - "a=1&b=2..." 형태의 쿼리 문자열 (맨 앞 ? 는 없어야 합니다)
|
|
||||||
* @param key - 설정할 파라미터 이름
|
|
||||||
* @param value - 설정할 값
|
|
||||||
* @returns - "a=1&b=2&c=3..." 형태의 새로운 쿼리 문자열
|
|
||||||
*/
|
|
||||||
setQueryParam(query: string, key: string, value: string): string {
|
|
||||||
query = query.toLowerCase();
|
|
||||||
key = this.decodeAndLower(key);
|
|
||||||
value = this.decodeAndLower(value);
|
|
||||||
|
|
||||||
const params = new URLSearchParams(query);
|
|
||||||
params.set(key, this.encodeAndLower(value));
|
|
||||||
return params.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 주어진 쿼리 문자열(query)에서 key에 해당하는 파라미터를 삭제(delete)하고,
|
|
||||||
* 전체 쿼리 문자열을 반환합니다.
|
|
||||||
* @param query - "a=1&b=2..." 형태의 쿼리 문자열 (맨 앞 ? 는 없어야 합니다)
|
|
||||||
* @param key - 삭제할 파라미터 이름
|
|
||||||
* @returns - 삭제된 상태의 새로운 쿼리 문자열
|
|
||||||
*/
|
|
||||||
removeQueryParam(query: string, key: string): string {
|
|
||||||
query = query.toLowerCase();
|
|
||||||
key = this.decodeAndLower(key);
|
|
||||||
|
|
||||||
const params = new URLSearchParams(query);
|
|
||||||
params.delete(key);
|
|
||||||
return params.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Headers
|
|
||||||
/**
|
|
||||||
* !! 만약 request.getHeader(`${key}`)을 사용할 수 있다면 이 함수를 사용하지 마세요.
|
|
||||||
* 주어진 헤더 맵에서 name에 해당하는 첫 번째 헤더 값을 반환합니다.
|
|
||||||
* @param headers - Response.getHeaders() 가 반환하는 객체
|
|
||||||
* @param name - 꺼내고 싶은 헤더 이름 (예: "location", "Content-Type")
|
|
||||||
* @returns - 해당 헤더의 첫 번째 값, 없으면 null
|
|
||||||
*/
|
|
||||||
getHeaderValue(
|
|
||||||
headers: Record<string, string | string[]>,
|
|
||||||
name: string
|
|
||||||
): string | null {
|
|
||||||
const normalized = this.lowerCaseAllHeaders(headers);
|
|
||||||
const target = name.toLowerCase();
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(normalized)) {
|
|
||||||
if (key === target) {
|
|
||||||
let rawValue: string | null = null;
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
rawValue = value.length > 0 && value[0] ? value[0] : null;
|
|
||||||
} else {
|
|
||||||
rawValue = value.length > 0 ? value : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rawValue !== null) {
|
|
||||||
try {
|
|
||||||
return decodeURIComponent(rawValue);
|
|
||||||
} catch {
|
|
||||||
return rawValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 주어진 헤더 맵에서 name에 해당하는 헤더 값을 value로 변경한 새 맵을 반환합니다.
|
|
||||||
* - 기존 헤더 이름의 대소문자를 보존합니다.
|
|
||||||
* - value가 string인 경우 [value] 형태로, string[]인 경우 그대로 사용합니다.
|
|
||||||
* - 기존에 해당 헤더가 없으면 새로 추가합니다.
|
|
||||||
*
|
|
||||||
* @param headers - 키가 헤더 이름, 값이 문자열 배열인 헤더 맵
|
|
||||||
* @param name - 변경할 헤더 이름 (예: "Authorization", "X-Custom-Header")
|
|
||||||
* @param value - 새로 설정할 값 (string 또는 string[])
|
|
||||||
* @returns - 지정된 헤더가 업데이트된 새로운 헤더 맵
|
|
||||||
*/
|
|
||||||
setHeaderValue(
|
|
||||||
headers: Record<string, string | string[]>,
|
|
||||||
name: string,
|
|
||||||
value: string | string[]
|
|
||||||
): Record<string, string[]> {
|
|
||||||
headers = this.lowerCaseAllHeaders(headers);
|
|
||||||
const lowerName = name.toLowerCase();
|
|
||||||
const newHeaders: Record<string, string[]> = {};
|
|
||||||
|
|
||||||
// 1) 기존 헤더 복사하되, name과 일치하는 항목은 value로 대체
|
|
||||||
for (const [key, vals] of Object.entries(headers)) {
|
|
||||||
if (key.toLowerCase() === lowerName) {
|
|
||||||
newHeaders[key] = Array.isArray(value) ? value : [value];
|
|
||||||
} else {
|
|
||||||
newHeaders[key] = Array.isArray(vals) ? vals : [vals];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) 해당 헤더가 원래 없었다면 새로 추가
|
|
||||||
const exists = Object.keys(newHeaders).some(
|
|
||||||
(k) => k.toLowerCase() === lowerName
|
|
||||||
);
|
|
||||||
if (!exists) {
|
|
||||||
newHeaders[name] = Array.isArray(value) ? value : [value];
|
|
||||||
}
|
|
||||||
|
|
||||||
return newHeaders;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 주어진 헤더 맵에서 특정 이름(들)에 해당하는 헤더를 제거한 새 맵을 반환합니다.
|
|
||||||
* @param headers - 키가 헤더 이름, 값이 문자열 배열인 헤더 맵
|
|
||||||
* @param namesToRemove - 제거할 헤더 이름(하나 혹은 배열). 대소문자 구분 없이 매칭됩니다.
|
|
||||||
* @returns - 지정된 헤더가 제외된 새로운 헤더 맵
|
|
||||||
*/
|
|
||||||
removeHeaders(
|
|
||||||
headers: Record<string, string | string[]>,
|
|
||||||
namesToRemove: string | string[]
|
|
||||||
): Record<string, string[]> {
|
|
||||||
headers = this.lowerCaseAllHeaders(headers);
|
|
||||||
const toRemove = Array.isArray(namesToRemove)
|
|
||||||
? namesToRemove.map((n) => n.toLowerCase())
|
|
||||||
: [namesToRemove.toLowerCase()];
|
|
||||||
|
|
||||||
const filtered: Record<string, string[]> = {};
|
|
||||||
for (const [key, vals] of Object.entries(headers)) {
|
|
||||||
if (!toRemove.includes(key.toLowerCase())) {
|
|
||||||
filtered[key] = Array.isArray(vals) ? vals : [vals];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filtered;
|
|
||||||
}
|
|
||||||
|
|
||||||
async resend(
|
|
||||||
sdk: SDK,
|
|
||||||
request: Request,
|
|
||||||
options?: {
|
|
||||||
headers?: Record<string, string | string[]>;
|
|
||||||
body?: Body;
|
|
||||||
method?: string;
|
|
||||||
query?: string;
|
|
||||||
}
|
|
||||||
): Promise<Response | null> {
|
|
||||||
try {
|
|
||||||
const spec = new RequestSpec(request.getUrl());
|
|
||||||
spec.setMethod(options?.method || request.getMethod() || "GET");
|
|
||||||
if (options?.query) {
|
|
||||||
spec.setQuery(options.query);
|
|
||||||
} else {
|
|
||||||
spec.setQuery(request.getQuery() || "");
|
|
||||||
}
|
|
||||||
|
|
||||||
const originBody = request.getBody();
|
|
||||||
if (options?.body) {
|
|
||||||
spec.setBody(options.body);
|
|
||||||
} else if (originBody) {
|
|
||||||
spec.setBody(originBody);
|
|
||||||
}
|
|
||||||
|
|
||||||
const headers = request.getHeaders();
|
|
||||||
if (options?.headers) {
|
|
||||||
// 기존 헤더에서 options.headers로 덮어쓰기
|
|
||||||
const newHeaders = this.lowerCaseAllHeaders({
|
|
||||||
...headers,
|
|
||||||
...options.headers,
|
|
||||||
});
|
|
||||||
for (const [key, value] of Object.entries(newHeaders)) {
|
|
||||||
spec.setHeader(key, Array.isArray(value) ? value.join(", ") : value);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 기존 헤더 그대로 사용
|
|
||||||
for (const [key, value] of Object.entries(headers)) {
|
|
||||||
spec.setHeader(key, Array.isArray(value) ? value.join(", ") : value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await sdk.requests.send(spec);
|
|
||||||
return result.response ?? null;
|
|
||||||
} catch (error) {
|
|
||||||
sdk.console.error(
|
|
||||||
`Error resending request to ${request.getUrl()}: ${String(error)}`
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async customFetch(
|
|
||||||
sdk: SDK,
|
|
||||||
url: string,
|
|
||||||
method?: string,
|
|
||||||
query?: string,
|
|
||||||
headers?: Record<string, string | string[]>,
|
|
||||||
body?: Body
|
|
||||||
): Promise<Response | null> {
|
|
||||||
try {
|
|
||||||
const spec = new RequestSpec(url);
|
|
||||||
spec.setMethod(method || "GET");
|
|
||||||
if (query) {
|
|
||||||
spec.setQuery(query);
|
|
||||||
}
|
|
||||||
if (body) {
|
|
||||||
spec.setBody(body);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(headers || {})) {
|
|
||||||
spec.setHeader(key, Array.isArray(value) ? value.join(", ") : value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await sdk.requests.send(spec);
|
|
||||||
return result.response ?? null;
|
|
||||||
} catch {
|
|
||||||
sdk.console.error(
|
|
||||||
`Error during custom fetch to ${url}: ${String(error)}`
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"types": ["@caido/sdk-backend"],
|
"types": ["@caido/sdk-backend"],
|
||||||
|
"lib": ["ESNext", "DOM"]
|
||||||
},
|
},
|
||||||
"include": ["./src/**/*.ts"]
|
"include": ["./src/**/*.ts"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
// app.js
|
|
||||||
const express = require("express");
|
|
||||||
const crypto = require("crypto");
|
|
||||||
const app = express();
|
|
||||||
const port = 8000;
|
|
||||||
|
|
||||||
// 콜백 엔드포인트 (정상 동작 시뮬레이션)
|
|
||||||
app.get("/callback", (req, res) => {
|
|
||||||
res.send(`
|
|
||||||
<h1>Callback Received</h1>
|
|
||||||
<p>Query Params:</p>
|
|
||||||
<pre>${JSON.stringify(req.query, null, 2)}</pre>
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1) state 파라미터를 무시하는 취약한 /authorize 엔드포인트
|
|
||||||
* - 클라이언트가 state를 보내도 무시
|
|
||||||
* - 리디렉트 시 state를 포함하지 않음
|
|
||||||
*/
|
|
||||||
app.get("/authorize/no-state", (req, res) => {
|
|
||||||
const clientId = req.query.client_id || "unknown-client";
|
|
||||||
const redirectUri = encodeURIComponent(
|
|
||||||
req.query.redirect_uri || `http://localhost:${port}/callback`
|
|
||||||
);
|
|
||||||
const code = "authcode-12345";
|
|
||||||
|
|
||||||
// state를 전혀 포함하지 않은 채로 리디렉트
|
|
||||||
const location = `${redirectUri}?code=${code}&client_id=${clientId}`;
|
|
||||||
res.set("Location", location);
|
|
||||||
res.status(302).send(`Redirecting to ${location}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 2) 클라이언트가 보낸 state와 다른 값을 넣는 취약한 /authorize 엔드포인트
|
|
||||||
* - 클라이언트가 보낸 state를 로그로 확인만 하고,
|
|
||||||
* 응답 Location에는 'wrong-state'를 삽입
|
|
||||||
*/
|
|
||||||
app.get("/authorize/mismatch-state", (req, res) => {
|
|
||||||
const clientId = req.query.client_id || "unknown-client";
|
|
||||||
const originalState = req.query.state;
|
|
||||||
const redirectUri = encodeURIComponent(
|
|
||||||
req.query.redirect_uri || `http://localhost:${port}/callback`
|
|
||||||
);
|
|
||||||
const code = "authcode-67890";
|
|
||||||
|
|
||||||
// 클라이언트 state와 다르게 'wrong-state'를 삽입
|
|
||||||
const wrongState = "wrong-state";
|
|
||||||
const location = `${redirectUri}?code=${code}&state=${wrongState}&client_id=${clientId}`;
|
|
||||||
res.set("Location", location);
|
|
||||||
res.status(302).send(`Redirecting to ${location}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 3) 랜덤 state를 생성하여 리다이렉트를 발생시키는 테스트용 엔드포인트
|
|
||||||
* - /authorize/reuse-state-test 로 접근할 때마다 새로운 16진수 state를 생성
|
|
||||||
* - 최초 요청에 OAuth 파라미터가 없으므로 isOauthUri(request) == false
|
|
||||||
* - 응답에 Location 헤더로 '...?state=<랜덤값>' 을 포함
|
|
||||||
* -> Caido 플러그인의 checkNonceReuse 로직에서 새로운 state가 발급되었는지,
|
|
||||||
* 재사용되었는지를 검증할 수 있음
|
|
||||||
* - 더하여 callback uri에서 해당 nonce의 유효성을 판단하지 않고 응답 시에 vuln
|
|
||||||
*/
|
|
||||||
app.get("/authorize/reuse-state-test", (req, res) => {
|
|
||||||
const state = crypto.randomBytes(16).toString("hex");
|
|
||||||
|
|
||||||
// 고정된 콜백 URI로 리다이렉트 (OAuth 파라미터는 여기서만 주입)
|
|
||||||
const location = `http://localhost:${port}/callback?state=${state}&client_id=123`;
|
|
||||||
res.set("Location", location);
|
|
||||||
res.status(302).send(`Redirecting to ${location}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.listen(port, () => {
|
|
||||||
console.log(
|
|
||||||
`Vulnerable OAuth test server listening at http://localhost:${port}`
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`1) No-State: http://localhost:${port}/authorize/no-state?client_id=abc&redirect_uri=http://localhost:${port}/callback`
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`2) Mismatch-State: http://localhost:${port}/authorize/mismatch-state?client_id=abc&state=xyz&redirect_uri=http://localhost:${port}/callback`
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`3) Reuse-State-Test: http://localhost:${port}/authorize/reuse-state-test`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
1167
playground/csrf/package-lock.json
generated
1167
playground/csrf/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,19 +0,0 @@
|
||||||
{
|
|
||||||
"name": "csrf",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
|
||||||
"start": "nodemon index.js"
|
|
||||||
},
|
|
||||||
"author": "",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"express": "^5.1.0"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"description": "",
|
|
||||||
"devDependencies": {
|
|
||||||
"nodemon": "^3.1.10"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
164
pnpm-lock.yaml
generated
164
pnpm-lock.yaml
generated
|
|
@ -7,34 +7,23 @@ settings:
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
|
||||||
'@types/jsonwebtoken':
|
|
||||||
specifier: ^9.0.9
|
|
||||||
version: 9.0.9
|
|
||||||
jsonwebtoken:
|
|
||||||
specifier: ^9.0.2
|
|
||||||
version: 9.0.2
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@caido-community/dev':
|
'@caido-community/dev':
|
||||||
specifier: ^0.1.3
|
specifier: ^0.1.3
|
||||||
version: 0.1.5(@types/node@22.15.29)(postcss@8.5.3)(typescript@5.5.4)
|
version: 0.1.6(postcss@8.5.3)(typescript@5.5.4)
|
||||||
'@caido/sdk-backend':
|
'@caido/sdk-backend':
|
||||||
specifier: ^0.48.1
|
specifier: ^0.46.0
|
||||||
version: 0.48.1
|
version: 0.46.0
|
||||||
typescript:
|
typescript:
|
||||||
specifier: 5.5.4
|
specifier: 5.5.4
|
||||||
version: 5.5.4
|
version: 5.5.4
|
||||||
|
|
||||||
packages/backend:
|
packages/backend: {}
|
||||||
devDependencies:
|
|
||||||
'@caido/sdk-backend':
|
|
||||||
specifier: ^0.46.0
|
|
||||||
version: 0.46.0
|
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
'@caido-community/dev@0.1.5':
|
'@caido-community/dev@0.1.6':
|
||||||
resolution: {integrity: sha512-mM+komusTlKNViTlAR055KS9pQJk+jKzbFlUb+MKaFuopMG6qS93z6PSz/08uc7u7/i+U7YiiQvaa7c1IXvl6Q==}
|
resolution: {integrity: sha512-WAWmPdEahh4e24sO4crt+nvqZryhKsy4yP5QYGoyUKqEYVAct5S/lI9fHdoIRQPJDSds3ayB6jgMKAlifd8BAg==}
|
||||||
engines: {node: '>=20', pnpm: '>=9'}
|
engines: {node: '>=20', pnpm: '>=9'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
|
@ -44,15 +33,9 @@ packages:
|
||||||
'@caido/quickjs-types@0.17.2':
|
'@caido/quickjs-types@0.17.2':
|
||||||
resolution: {integrity: sha512-5kcucGORMNEbcdU91yKLYZG/TFDqsO6XmCZ1TnU6V48E61mmqrJg6kjrfOFP1WOugDm+ZcGd/Su3p3XkFXfaPg==}
|
resolution: {integrity: sha512-5kcucGORMNEbcdU91yKLYZG/TFDqsO6XmCZ1TnU6V48E61mmqrJg6kjrfOFP1WOugDm+ZcGd/Su3p3XkFXfaPg==}
|
||||||
|
|
||||||
'@caido/quickjs-types@0.18.0':
|
|
||||||
resolution: {integrity: sha512-hRXUVdDvlhEhvkBoWWytoVS2j1KDVZa8dx2Q/KvWUQTR57U8EMSYE9iFgvPhu78gS8z+RF42Zcb7moNx4SDMlw==}
|
|
||||||
|
|
||||||
'@caido/sdk-backend@0.46.0':
|
'@caido/sdk-backend@0.46.0':
|
||||||
resolution: {integrity: sha512-peUKW/4Nrw9WVxIahc+6KrVtxA7vsbpuJqOoBxudxq7tQJ+cV9IEqzvYoFFo8KlnrTkeUQUJvd0W4WsM3HgxEg==}
|
resolution: {integrity: sha512-peUKW/4Nrw9WVxIahc+6KrVtxA7vsbpuJqOoBxudxq7tQJ+cV9IEqzvYoFFo8KlnrTkeUQUJvd0W4WsM3HgxEg==}
|
||||||
|
|
||||||
'@caido/sdk-backend@0.48.1':
|
|
||||||
resolution: {integrity: sha512-JvFeOlSqAKbj3OenBn0LPtCNaOV0x6YtaAQijpvYfBJK32Nvbf924Z10bFVCu+Clc5A1qr7HcAvJ/8B/aRikWA==}
|
|
||||||
|
|
||||||
'@caido/sdk-shared@0.1.1':
|
'@caido/sdk-shared@0.1.1':
|
||||||
resolution: {integrity: sha512-JAV5ajUqxZdXYPTmDEvIKBZon8I5uHq44ATj0Nj3BVpllRDUGY9kcBd+PXMD50+3lv1CvhR3/f6q24T0+4aVJQ==}
|
resolution: {integrity: sha512-JAV5ajUqxZdXYPTmDEvIKBZon8I5uHq44ATj0Nj3BVpllRDUGY9kcBd+PXMD50+3lv1CvhR3/f6q24T0+4aVJQ==}
|
||||||
|
|
||||||
|
|
@ -335,15 +318,6 @@ packages:
|
||||||
'@types/estree@1.0.7':
|
'@types/estree@1.0.7':
|
||||||
resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
|
resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
|
||||||
|
|
||||||
'@types/jsonwebtoken@9.0.9':
|
|
||||||
resolution: {integrity: sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==}
|
|
||||||
|
|
||||||
'@types/ms@2.1.0':
|
|
||||||
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
|
||||||
|
|
||||||
'@types/node@22.15.29':
|
|
||||||
resolution: {integrity: sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==}
|
|
||||||
|
|
||||||
accepts@2.0.0:
|
accepts@2.0.0:
|
||||||
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
|
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
@ -380,9 +354,6 @@ packages:
|
||||||
brace-expansion@2.0.1:
|
brace-expansion@2.0.1:
|
||||||
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
|
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
|
||||||
|
|
||||||
buffer-equal-constant-time@1.0.1:
|
|
||||||
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
|
||||||
|
|
||||||
bundle-require@5.1.0:
|
bundle-require@5.1.0:
|
||||||
resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==}
|
resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==}
|
||||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
|
|
@ -484,9 +455,6 @@ packages:
|
||||||
eastasianwidth@0.2.0:
|
eastasianwidth@0.2.0:
|
||||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||||
|
|
||||||
ecdsa-sig-formatter@1.0.11:
|
|
||||||
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
|
|
||||||
|
|
||||||
ee-first@1.1.1:
|
ee-first@1.1.1:
|
||||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||||
|
|
||||||
|
|
@ -629,8 +597,8 @@ packages:
|
||||||
jackspeak@3.4.3:
|
jackspeak@3.4.3:
|
||||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||||
|
|
||||||
jackspeak@4.1.0:
|
jackspeak@4.1.1:
|
||||||
resolution: {integrity: sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==}
|
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
jiti@2.4.2:
|
jiti@2.4.2:
|
||||||
|
|
@ -644,19 +612,9 @@ packages:
|
||||||
json-schema-traverse@1.0.0:
|
json-schema-traverse@1.0.0:
|
||||||
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
||||||
|
|
||||||
jsonwebtoken@9.0.2:
|
|
||||||
resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
|
|
||||||
engines: {node: '>=12', npm: '>=6'}
|
|
||||||
|
|
||||||
jszip@3.10.1:
|
jszip@3.10.1:
|
||||||
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
|
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
|
||||||
|
|
||||||
jwa@1.4.2:
|
|
||||||
resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==}
|
|
||||||
|
|
||||||
jws@3.2.2:
|
|
||||||
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
|
|
||||||
|
|
||||||
lie@3.3.0:
|
lie@3.3.0:
|
||||||
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
|
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
|
||||||
|
|
||||||
|
|
@ -671,27 +629,6 @@ packages:
|
||||||
resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==}
|
resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==}
|
||||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
|
|
||||||
lodash.includes@4.3.0:
|
|
||||||
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
|
|
||||||
|
|
||||||
lodash.isboolean@3.0.3:
|
|
||||||
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
|
|
||||||
|
|
||||||
lodash.isinteger@4.0.4:
|
|
||||||
resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
|
|
||||||
|
|
||||||
lodash.isnumber@3.0.3:
|
|
||||||
resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
|
|
||||||
|
|
||||||
lodash.isplainobject@4.0.6:
|
|
||||||
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
|
|
||||||
|
|
||||||
lodash.isstring@4.0.1:
|
|
||||||
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
|
|
||||||
|
|
||||||
lodash.once@4.1.1:
|
|
||||||
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
|
|
||||||
|
|
||||||
lodash.sortby@4.7.0:
|
lodash.sortby@4.7.0:
|
||||||
resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==}
|
resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==}
|
||||||
|
|
||||||
|
|
@ -890,11 +827,6 @@ packages:
|
||||||
safer-buffer@2.1.2:
|
safer-buffer@2.1.2:
|
||||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||||
|
|
||||||
semver@7.7.2:
|
|
||||||
resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
send@1.2.0:
|
send@1.2.0:
|
||||||
resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==}
|
resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==}
|
||||||
engines: {node: '>= 18'}
|
engines: {node: '>= 18'}
|
||||||
|
|
@ -1029,9 +961,6 @@ packages:
|
||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
undici-types@6.21.0:
|
|
||||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
|
||||||
|
|
||||||
unpipe@1.0.0:
|
unpipe@1.0.0:
|
||||||
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
@ -1126,7 +1055,7 @@ packages:
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@caido-community/dev@0.1.5(@types/node@22.15.29)(postcss@8.5.3)(typescript@5.5.4)':
|
'@caido-community/dev@0.1.6(postcss@8.5.3)(typescript@5.5.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@caido/plugin-manifest': 0.3.0
|
'@caido/plugin-manifest': 0.3.0
|
||||||
chalk: 5.4.1
|
chalk: 5.4.1
|
||||||
|
|
@ -1137,7 +1066,7 @@ snapshots:
|
||||||
jiti: 2.4.2
|
jiti: 2.4.2
|
||||||
jszip: 3.10.1
|
jszip: 3.10.1
|
||||||
tsup: 8.3.5(jiti@2.4.2)(postcss@8.5.3)(typescript@5.5.4)
|
tsup: 8.3.5(jiti@2.4.2)(postcss@8.5.3)(typescript@5.5.4)
|
||||||
vite: 6.0.7(@types/node@22.15.29)(jiti@2.4.2)
|
vite: 6.0.7(jiti@2.4.2)
|
||||||
ws: 8.18.0
|
ws: 8.18.0
|
||||||
zod: 3.24.1
|
zod: 3.24.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
|
@ -1165,18 +1094,11 @@ snapshots:
|
||||||
|
|
||||||
'@caido/quickjs-types@0.17.2': {}
|
'@caido/quickjs-types@0.17.2': {}
|
||||||
|
|
||||||
'@caido/quickjs-types@0.18.0': {}
|
|
||||||
|
|
||||||
'@caido/sdk-backend@0.46.0':
|
'@caido/sdk-backend@0.46.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@caido/quickjs-types': 0.17.2
|
'@caido/quickjs-types': 0.17.2
|
||||||
'@caido/sdk-shared': 0.1.1
|
'@caido/sdk-shared': 0.1.1
|
||||||
|
|
||||||
'@caido/sdk-backend@0.48.1':
|
|
||||||
dependencies:
|
|
||||||
'@caido/quickjs-types': 0.18.0
|
|
||||||
'@caido/sdk-shared': 0.1.1
|
|
||||||
|
|
||||||
'@caido/sdk-shared@0.1.1': {}
|
'@caido/sdk-shared@0.1.1': {}
|
||||||
|
|
||||||
'@esbuild/aix-ppc64@0.24.2':
|
'@esbuild/aix-ppc64@0.24.2':
|
||||||
|
|
@ -1345,17 +1267,6 @@ snapshots:
|
||||||
|
|
||||||
'@types/estree@1.0.7': {}
|
'@types/estree@1.0.7': {}
|
||||||
|
|
||||||
'@types/jsonwebtoken@9.0.9':
|
|
||||||
dependencies:
|
|
||||||
'@types/ms': 2.1.0
|
|
||||||
'@types/node': 22.15.29
|
|
||||||
|
|
||||||
'@types/ms@2.1.0': {}
|
|
||||||
|
|
||||||
'@types/node@22.15.29':
|
|
||||||
dependencies:
|
|
||||||
undici-types: 6.21.0
|
|
||||||
|
|
||||||
accepts@2.0.0:
|
accepts@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
mime-types: 3.0.1
|
mime-types: 3.0.1
|
||||||
|
|
@ -1400,8 +1311,6 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
balanced-match: 1.0.2
|
balanced-match: 1.0.2
|
||||||
|
|
||||||
buffer-equal-constant-time@1.0.1: {}
|
|
||||||
|
|
||||||
bundle-require@5.1.0(esbuild@0.24.2):
|
bundle-require@5.1.0(esbuild@0.24.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.24.2
|
esbuild: 0.24.2
|
||||||
|
|
@ -1475,10 +1384,6 @@ snapshots:
|
||||||
|
|
||||||
eastasianwidth@0.2.0: {}
|
eastasianwidth@0.2.0: {}
|
||||||
|
|
||||||
ecdsa-sig-formatter@1.0.11:
|
|
||||||
dependencies:
|
|
||||||
safe-buffer: 5.2.1
|
|
||||||
|
|
||||||
ee-first@1.1.1: {}
|
ee-first@1.1.1: {}
|
||||||
|
|
||||||
emoji-regex@8.0.0: {}
|
emoji-regex@8.0.0: {}
|
||||||
|
|
@ -1627,7 +1532,7 @@ snapshots:
|
||||||
glob@11.0.1:
|
glob@11.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
foreground-child: 3.3.1
|
foreground-child: 3.3.1
|
||||||
jackspeak: 4.1.0
|
jackspeak: 4.1.1
|
||||||
minimatch: 10.0.1
|
minimatch: 10.0.1
|
||||||
minipass: 7.1.2
|
minipass: 7.1.2
|
||||||
package-json-from-dist: 1.0.1
|
package-json-from-dist: 1.0.1
|
||||||
|
|
@ -1673,7 +1578,7 @@ snapshots:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@pkgjs/parseargs': 0.11.0
|
'@pkgjs/parseargs': 0.11.0
|
||||||
|
|
||||||
jackspeak@4.1.0:
|
jackspeak@4.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@isaacs/cliui': 8.0.2
|
'@isaacs/cliui': 8.0.2
|
||||||
|
|
||||||
|
|
@ -1683,19 +1588,6 @@ snapshots:
|
||||||
|
|
||||||
json-schema-traverse@1.0.0: {}
|
json-schema-traverse@1.0.0: {}
|
||||||
|
|
||||||
jsonwebtoken@9.0.2:
|
|
||||||
dependencies:
|
|
||||||
jws: 3.2.2
|
|
||||||
lodash.includes: 4.3.0
|
|
||||||
lodash.isboolean: 3.0.3
|
|
||||||
lodash.isinteger: 4.0.4
|
|
||||||
lodash.isnumber: 3.0.3
|
|
||||||
lodash.isplainobject: 4.0.6
|
|
||||||
lodash.isstring: 4.0.1
|
|
||||||
lodash.once: 4.1.1
|
|
||||||
ms: 2.1.3
|
|
||||||
semver: 7.7.2
|
|
||||||
|
|
||||||
jszip@3.10.1:
|
jszip@3.10.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
lie: 3.3.0
|
lie: 3.3.0
|
||||||
|
|
@ -1703,17 +1595,6 @@ snapshots:
|
||||||
readable-stream: 2.3.8
|
readable-stream: 2.3.8
|
||||||
setimmediate: 1.0.5
|
setimmediate: 1.0.5
|
||||||
|
|
||||||
jwa@1.4.2:
|
|
||||||
dependencies:
|
|
||||||
buffer-equal-constant-time: 1.0.1
|
|
||||||
ecdsa-sig-formatter: 1.0.11
|
|
||||||
safe-buffer: 5.2.1
|
|
||||||
|
|
||||||
jws@3.2.2:
|
|
||||||
dependencies:
|
|
||||||
jwa: 1.4.2
|
|
||||||
safe-buffer: 5.2.1
|
|
||||||
|
|
||||||
lie@3.3.0:
|
lie@3.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
immediate: 3.0.6
|
immediate: 3.0.6
|
||||||
|
|
@ -1724,20 +1605,6 @@ snapshots:
|
||||||
|
|
||||||
load-tsconfig@0.2.5: {}
|
load-tsconfig@0.2.5: {}
|
||||||
|
|
||||||
lodash.includes@4.3.0: {}
|
|
||||||
|
|
||||||
lodash.isboolean@3.0.3: {}
|
|
||||||
|
|
||||||
lodash.isinteger@4.0.4: {}
|
|
||||||
|
|
||||||
lodash.isnumber@3.0.3: {}
|
|
||||||
|
|
||||||
lodash.isplainobject@4.0.6: {}
|
|
||||||
|
|
||||||
lodash.isstring@4.0.1: {}
|
|
||||||
|
|
||||||
lodash.once@4.1.1: {}
|
|
||||||
|
|
||||||
lodash.sortby@4.7.0: {}
|
lodash.sortby@4.7.0: {}
|
||||||
|
|
||||||
lru-cache@10.4.3: {}
|
lru-cache@10.4.3: {}
|
||||||
|
|
@ -1917,8 +1784,6 @@ snapshots:
|
||||||
|
|
||||||
safer-buffer@2.1.2: {}
|
safer-buffer@2.1.2: {}
|
||||||
|
|
||||||
semver@7.7.2: {}
|
|
||||||
|
|
||||||
send@1.2.0:
|
send@1.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.3.6
|
debug: 4.3.6
|
||||||
|
|
@ -2086,8 +1951,6 @@ snapshots:
|
||||||
|
|
||||||
typescript@5.5.4: {}
|
typescript@5.5.4: {}
|
||||||
|
|
||||||
undici-types@6.21.0: {}
|
|
||||||
|
|
||||||
unpipe@1.0.0: {}
|
unpipe@1.0.0: {}
|
||||||
|
|
||||||
util-deprecate@1.0.2: {}
|
util-deprecate@1.0.2: {}
|
||||||
|
|
@ -2096,13 +1959,12 @@ snapshots:
|
||||||
|
|
||||||
vary@1.1.2: {}
|
vary@1.1.2: {}
|
||||||
|
|
||||||
vite@6.0.7(@types/node@22.15.29)(jiti@2.4.2):
|
vite@6.0.7(jiti@2.4.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.24.2
|
esbuild: 0.24.2
|
||||||
postcss: 8.5.3
|
postcss: 8.5.3
|
||||||
rollup: 4.41.0
|
rollup: 4.41.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 22.15.29
|
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
jiti: 2.4.2
|
jiti: 2.4.2
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue