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:
shadcn
2025-07-11 18:19:34 +04:00
committed by GitHub
parent 06d03d64f4
commit 6c341c16ae
9 changed files with 134 additions and 16 deletions

View File

@@ -0,0 +1,5 @@
---
"shadcn": patch
---
fix safe target handling

View File

@@ -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.

View File

@@ -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": "..."
}
]
}
```

View File

@@ -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).

View File

@@ -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

View File

@@ -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."
},

View File

@@ -13,6 +13,7 @@ export const registryItemTypeSchema = z.enum([
"registry:file",
"registry:theme",
"registry:style",
"registry:item",
// Internal use only
"registry:example",

View File

@@ -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)
})

View File

@@ -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