mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-29 23:55:02 +00:00
feat: fix safe target and add docs (#7795)
* feat: fix safe target and add docs * chore: add changeset * fix: changelog
This commit is contained in:
5
.changeset/cyan-elephants-hear.md
Normal file
5
.changeset/cyan-elephants-hear.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"shadcn": patch
|
||||
---
|
||||
|
||||
fix safe target handling
|
||||
@@ -4,6 +4,14 @@ description: Latest updates and announcements.
|
||||
toc: false
|
||||
---
|
||||
|
||||
## July 2025 - Universal Registry Items
|
||||
|
||||
We've added support for universal registry items. This allows you to create registry items that can be distributed to any project i.e. no framework, no components.json, no tailwind, no react required.
|
||||
|
||||
This new registry item type unlocks a lot of new workflows. You can now distribute code, config, rules, docs, anything to any code project.
|
||||
|
||||
See the [docs](/docs/registry/examples) for more details and examples.
|
||||
|
||||
## July 2025 - Local File Support
|
||||
|
||||
The shadcn CLI now supports local files. Initialize projects and add components, themes, hooks, utils and more from local JSON files.
|
||||
|
||||
@@ -368,3 +368,70 @@ Note: you need to define both `@keyframes` in css and `theme` in cssVars to use
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Universal Items
|
||||
|
||||
As of `2.9.0`, you can create universal items that can be installed without framework detection or components.json.
|
||||
|
||||
To make an item universal i.e framework agnostic, all the files in the item must have an explicit target.
|
||||
|
||||
Here's an example of a registry item that installs custom Cursor rules for _python_:
|
||||
|
||||
```json title=".cursor/rules/custom-python.mdc" showLineNumbers {9}
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "python-rules",
|
||||
"type": "registry:item",
|
||||
"files": [
|
||||
{
|
||||
"path": "/path/to/your/registry/default/custom-python.mdc",
|
||||
"type": "registry:file",
|
||||
"target": "~/.cursor/rules/custom-python.mdc",
|
||||
"content": "..."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Here's another example for installation custom ESLint config:
|
||||
|
||||
```json title=".eslintrc.json" showLineNumbers {9}
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "my-eslint-config",
|
||||
"type": "registry:item",
|
||||
"files": [
|
||||
{
|
||||
"path": "/path/to/your/registry/default/custom-eslint.json",
|
||||
"type": "registry:file",
|
||||
"target": "~/.eslintrc.json",
|
||||
"content": "..."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
You can also have a universal item that installs multiple files:
|
||||
|
||||
```json title="my-custom-starter-template.json" showLineNumbers {9}
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "my-custom-start-template",
|
||||
"type": "registry:item",
|
||||
dependencies: ["better-auth"]
|
||||
"files": [
|
||||
{
|
||||
"path": "/path/to/file-01.json",
|
||||
"type": "registry:file",
|
||||
"target": "~/file-01.json",
|
||||
"content": "..."
|
||||
},
|
||||
{
|
||||
"path": "/path/to/file-02.vue",
|
||||
"type": "registry:file",
|
||||
"target": "~/pages/file-02.vue",
|
||||
"content": "..."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
---
|
||||
title: Registry
|
||||
description: Run your own component registry.
|
||||
description: Run your own code registry.
|
||||
---
|
||||
|
||||
<Callout>
|
||||
**Note:** This feature is currently experimental. Help us improve it by
|
||||
testing it out and sending feedback. If you have any questions, please [reach
|
||||
out to us](https://github.com/shadcn-ui/ui/discussions).
|
||||
</Callout>
|
||||
You can use the `shadcn` CLI to run your own code registry. Running your own registry allows you to distribute your custom components, hooks, pages, config, rules and other files to any project.
|
||||
|
||||
You can use the `shadcn` CLI to run your own component registry. Running your own registry allows you to distribute your custom components, hooks, pages, and other files to any React project.
|
||||
<Callout>
|
||||
**Note:** The registry works with any project type and any framework, and is
|
||||
not limited to React.
|
||||
</Callout>
|
||||
|
||||
<figure className="flex flex-col gap-4">
|
||||
<Image
|
||||
@@ -27,12 +26,10 @@ You can use the `shadcn` CLI to run your own component registry. Running your ow
|
||||
className="mt-6 hidden w-full overflow-hidden rounded-lg border shadow-sm dark:block"
|
||||
/>
|
||||
<figcaption className="text-center text-sm text-gray-500">
|
||||
Distribute code to any React project.
|
||||
A distribution system for code
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
Registry items are automatically compatible with the `shadcn` CLI and `Open in v0`.
|
||||
|
||||
## Requirements
|
||||
|
||||
You are free to design and host your custom registry as you see fit. The only requirement is that your registry items must be valid JSON files that conform to the [registry-item schema specification](/docs/registry/registry-item-json).
|
||||
|
||||
@@ -107,6 +107,7 @@ The following types are supported:
|
||||
| `registry:file` | Use for miscellaneous files. |
|
||||
| `registry:style` | Use for registry styles. eg. `new-york` |
|
||||
| `registry:theme` | Use for themes. |
|
||||
| `registry:item` | Use for universal registry items. |
|
||||
|
||||
### author
|
||||
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
"registry:theme",
|
||||
"registry:page",
|
||||
"registry:file",
|
||||
"registry:style"
|
||||
"registry:style",
|
||||
"registry:item"
|
||||
],
|
||||
"description": "The type of the item. This is used to determine the type and target path of the item when resolved for a project."
|
||||
},
|
||||
@@ -79,7 +80,8 @@
|
||||
"registry:theme",
|
||||
"registry:page",
|
||||
"registry:file",
|
||||
"registry:style"
|
||||
"registry:style",
|
||||
"registry:item"
|
||||
],
|
||||
"description": "The type of the file. This is used to determine the type of the file when resolved for a project."
|
||||
},
|
||||
|
||||
@@ -13,6 +13,7 @@ export const registryItemTypeSchema = z.enum([
|
||||
"registry:file",
|
||||
"registry:theme",
|
||||
"registry:style",
|
||||
"registry:item",
|
||||
|
||||
// Internal use only
|
||||
"registry:example",
|
||||
|
||||
@@ -67,6 +67,11 @@ describe("isSafeTarget", () => {
|
||||
description: "Unicode normalization attacks",
|
||||
target: "foo/../\u2025/etc/passwd",
|
||||
},
|
||||
{
|
||||
description:
|
||||
"path traversal with square brackets outside [...] pattern",
|
||||
target: "foo/[bar]/../../etc/passwd",
|
||||
},
|
||||
])("$description", ({ target }) => {
|
||||
expect(isSafeTarget(target, cwd)).toBe(false)
|
||||
})
|
||||
@@ -102,6 +107,26 @@ describe("isSafeTarget", () => {
|
||||
description: "path with special characters",
|
||||
target: "components/@ui/button.tsx",
|
||||
},
|
||||
{
|
||||
description: "framework routing with square brackets",
|
||||
target: "pages/[id].tsx",
|
||||
},
|
||||
{
|
||||
description: "catch-all routes with [...param]",
|
||||
target: "server/api/auth/[...].ts",
|
||||
},
|
||||
{
|
||||
description: "optional catch-all routes",
|
||||
target: "pages/[[...slug]].tsx",
|
||||
},
|
||||
{
|
||||
description: "dollar sign routes",
|
||||
target: "routes/$userId.tsx",
|
||||
},
|
||||
{
|
||||
description: "complex routing patterns",
|
||||
target: "app/[locale]/[...segments]/page.tsx",
|
||||
},
|
||||
])("$description", ({ target }) => {
|
||||
expect(isSafeTarget(target, cwd)).toBe(true)
|
||||
})
|
||||
|
||||
@@ -27,15 +27,27 @@ export function isSafeTarget(targetPath: string, cwd: string): boolean {
|
||||
const normalizedRoot = path.normalize(cwd)
|
||||
|
||||
// Check for explicit path traversal sequences in both encoded and decoded forms.
|
||||
// Allow [...] pattern which is common in framework routing (e.g., [...slug])
|
||||
const hasPathTraversal = (path: string) => {
|
||||
// Remove [...] patterns before checking for ..
|
||||
const withoutBrackets = path.replace(/\[\.\.\..*?\]/g, "")
|
||||
return withoutBrackets.includes("..")
|
||||
}
|
||||
|
||||
if (
|
||||
normalizedTarget.includes("..") ||
|
||||
decodedPath.includes("..") ||
|
||||
targetPath.includes("..")
|
||||
hasPathTraversal(normalizedTarget) ||
|
||||
hasPathTraversal(decodedPath) ||
|
||||
hasPathTraversal(targetPath)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for current directory references that might be used in traversal.
|
||||
// First, remove [...] patterns to avoid false positives
|
||||
const cleanPath = (path: string) => path.replace(/\[\.\.\..*?\]/g, "")
|
||||
const cleanTarget = cleanPath(targetPath)
|
||||
const cleanDecoded = cleanPath(decodedPath)
|
||||
|
||||
const suspiciousPatterns = [
|
||||
/\.\.[\/\\]/, // ../ or ..\
|
||||
/[\/\\]\.\./, // /.. or \..
|
||||
@@ -47,7 +59,7 @@ export function isSafeTarget(targetPath: string, cwd: string): boolean {
|
||||
|
||||
if (
|
||||
suspiciousPatterns.some(
|
||||
(pattern) => pattern.test(targetPath) || pattern.test(decodedPath)
|
||||
(pattern) => pattern.test(cleanTarget) || pattern.test(cleanDecoded)
|
||||
)
|
||||
) {
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user