Remove version param, add path traversal protection
This commit is contained in:
parent
6e5d665846
commit
c081785d37
|
|
@ -3,11 +3,17 @@ import { define, stylesToCSS } from '@because/forge'
|
|||
import { baseStyles, ToolScript, theme } from '@because/toes/tools'
|
||||
import { readdir, stat } from 'fs/promises'
|
||||
import { readFileSync } from 'fs'
|
||||
import { join, extname, basename } from 'path'
|
||||
import { join, resolve, extname, basename } from 'path'
|
||||
import type { Child } from 'hono/jsx'
|
||||
|
||||
const APPS_DIR = process.env.APPS_DIR!
|
||||
|
||||
const safePath = (base: string, ...segments: string[]) => {
|
||||
const full = resolve(base, ...segments)
|
||||
if (!full.startsWith(base + '/') && full !== base) return null
|
||||
return full
|
||||
}
|
||||
|
||||
const app = new Hype({ prettyHTML: false })
|
||||
|
||||
const Container = define('Container', {
|
||||
|
|
@ -257,21 +263,15 @@ const fileMemoryScript = `
|
|||
var params = new URLSearchParams(window.location.search);
|
||||
var app = params.get('app');
|
||||
var file = params.get('file');
|
||||
var version = params.get('version') || 'current';
|
||||
if (!app) return;
|
||||
var key = 'code-app:' + app + ':' + version + ':file';
|
||||
var key = 'code-app:' + app + ':file';
|
||||
if (params.has('file')) {
|
||||
// Explicit file param (even empty) - save it
|
||||
if (file) localStorage.setItem(key, file);
|
||||
else localStorage.removeItem(key);
|
||||
} else {
|
||||
// No file param - restore saved location
|
||||
var saved = localStorage.getItem(key);
|
||||
if (saved) {
|
||||
var url = '/?app=' + encodeURIComponent(app);
|
||||
if (version !== 'current') url += '&version=' + encodeURIComponent(version);
|
||||
url += '&file=' + encodeURIComponent(saved);
|
||||
window.location.replace(url);
|
||||
window.location.replace('/?app=' + encodeURIComponent(app) + '&file=' + encodeURIComponent(saved));
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
|
@ -327,14 +327,14 @@ app.get('/styles.css', c => c.text(baseStyles + stylesToCSS(), 200, {
|
|||
|
||||
app.get('/raw', async c => {
|
||||
const appName = c.req.query('app')
|
||||
const version = c.req.query('version') || 'current'
|
||||
const filePath = c.req.query('file')
|
||||
|
||||
if (!appName || !filePath) {
|
||||
return c.text('Missing app or file parameter', 400)
|
||||
}
|
||||
|
||||
const fullPath = join(APPS_DIR, appName, version, filePath)
|
||||
const fullPath = safePath(APPS_DIR, appName, filePath)
|
||||
if (!fullPath) return c.text('Invalid path', 400)
|
||||
const file = Bun.file(fullPath)
|
||||
|
||||
if (!await file.exists()) {
|
||||
|
|
@ -346,14 +346,14 @@ app.get('/raw', async c => {
|
|||
|
||||
app.post('/save', async c => {
|
||||
const appName = c.req.query('app')
|
||||
const version = c.req.query('version') || 'current'
|
||||
const filePath = c.req.query('file')
|
||||
|
||||
if (!appName || !filePath) {
|
||||
return c.text('Missing app or file parameter', 400)
|
||||
}
|
||||
|
||||
const fullPath = join(APPS_DIR, appName, version, filePath)
|
||||
const fullPath = safePath(APPS_DIR, appName, filePath)
|
||||
if (!fullPath) return c.text('Invalid path', 400)
|
||||
const content = await c.req.text()
|
||||
|
||||
try {
|
||||
|
|
@ -385,10 +385,9 @@ async function listFiles(appPath: string, subPath: string = '') {
|
|||
interface BreadcrumbProps {
|
||||
appName: string
|
||||
filePath: string
|
||||
versionParam: string
|
||||
}
|
||||
|
||||
function PathBreadcrumb({ appName, filePath, versionParam }: BreadcrumbProps) {
|
||||
function PathBreadcrumb({ appName, filePath }: BreadcrumbProps) {
|
||||
const parts = filePath ? filePath.split('/').filter(Boolean) : []
|
||||
const crumbs: { name: string; path: string }[] = []
|
||||
|
||||
|
|
@ -401,7 +400,7 @@ function PathBreadcrumb({ appName, filePath, versionParam }: BreadcrumbProps) {
|
|||
return (
|
||||
<Breadcrumb>
|
||||
{crumbs.length > 0 ? (
|
||||
<BreadcrumbLink href={`/?app=${appName}${versionParam}&file=`}>{appName}</BreadcrumbLink>
|
||||
<BreadcrumbLink href={`/?app=${appName}&file=`}>{appName}</BreadcrumbLink>
|
||||
) : (
|
||||
<BreadcrumbCurrent>{appName}</BreadcrumbCurrent>
|
||||
)}
|
||||
|
|
@ -411,7 +410,7 @@ function PathBreadcrumb({ appName, filePath, versionParam }: BreadcrumbProps) {
|
|||
{i === crumbs.length - 1 ? (
|
||||
<BreadcrumbCurrent>{crumb.name}</BreadcrumbCurrent>
|
||||
) : (
|
||||
<BreadcrumbLink href={`/?app=${appName}${versionParam}&file=${crumb.path}`}>{crumb.name}</BreadcrumbLink>
|
||||
<BreadcrumbLink href={`/?app=${appName}&file=${crumb.path}`}>{crumb.name}</BreadcrumbLink>
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
|
|
@ -479,7 +478,6 @@ function getPrismLanguage(filename: string): string {
|
|||
|
||||
app.get('/', async c => {
|
||||
const appName = c.req.query('app')
|
||||
const version = c.req.query('version') || 'current'
|
||||
const filePath = c.req.query('file') || ''
|
||||
|
||||
if (!appName) {
|
||||
|
|
@ -490,19 +488,34 @@ app.get('/', async c => {
|
|||
)
|
||||
}
|
||||
|
||||
const appPath = join(APPS_DIR, appName, version)
|
||||
const appPath = safePath(APPS_DIR, appName)
|
||||
if (!appPath) {
|
||||
return c.html(
|
||||
<Layout title="Code Browser">
|
||||
<ErrorBox>Invalid app name</ErrorBox>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
await stat(appPath)
|
||||
} catch {
|
||||
return c.html(
|
||||
<Layout title="Code Browser">
|
||||
<ErrorBox>App "{appName}" (version: {version}) not found</ErrorBox>
|
||||
<ErrorBox>App "{appName}" not found</ErrorBox>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
const fullPath = safePath(appPath, filePath)
|
||||
if (!fullPath) {
|
||||
return c.html(
|
||||
<Layout title="Code Browser">
|
||||
<ErrorBox>Invalid file path</ErrorBox>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
const fullPath = join(appPath, filePath)
|
||||
let fileStats
|
||||
|
||||
try {
|
||||
|
|
@ -515,18 +528,16 @@ app.get('/', async c => {
|
|||
)
|
||||
}
|
||||
|
||||
const versionParam = version !== 'current' ? `&version=${version}` : ''
|
||||
|
||||
if (fileStats.isFile()) {
|
||||
const filename = basename(fullPath)
|
||||
const fileType = getFileType(filename)
|
||||
const rawUrl = `/raw?app=${appName}${versionParam}&file=${filePath}`
|
||||
const rawUrl = `/raw?app=${appName}&file=${filePath}`
|
||||
const downloadUrl = `${rawUrl}&download=1`
|
||||
|
||||
if (fileType === 'image') {
|
||||
return c.html(
|
||||
<Layout title={`${appName}/${filePath}`}>
|
||||
<PathBreadcrumb appName={appName} filePath={filePath} versionParam={versionParam} />
|
||||
<PathBreadcrumb appName={appName} filePath={filePath} />
|
||||
<MediaContainer>
|
||||
<MediaHeader>
|
||||
<span>{filename}</span>
|
||||
|
|
@ -543,7 +554,7 @@ app.get('/', async c => {
|
|||
if (fileType === 'audio') {
|
||||
return c.html(
|
||||
<Layout title={`${appName}/${filePath}`}>
|
||||
<PathBreadcrumb appName={appName} filePath={filePath} versionParam={versionParam} />
|
||||
<PathBreadcrumb appName={appName} filePath={filePath} />
|
||||
<MediaContainer>
|
||||
<MediaHeader>
|
||||
<span>{filename}</span>
|
||||
|
|
@ -560,7 +571,7 @@ app.get('/', async c => {
|
|||
if (fileType === 'video') {
|
||||
return c.html(
|
||||
<Layout title={`${appName}/${filePath}`}>
|
||||
<PathBreadcrumb appName={appName} filePath={filePath} versionParam={versionParam} />
|
||||
<PathBreadcrumb appName={appName} filePath={filePath} />
|
||||
<MediaContainer>
|
||||
<MediaHeader>
|
||||
<span>{filename}</span>
|
||||
|
|
@ -615,7 +626,7 @@ saveBtn.onclick = async () => {
|
|||
status.textContent = 'Saving...';
|
||||
|
||||
try {
|
||||
const res = await fetch('/save?app=${appName}${versionParam}&file=${filePath}', {
|
||||
const res = await fetch('/save?app=${appName}&file=${filePath}', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
body: jar.toString()
|
||||
|
|
@ -641,14 +652,14 @@ document.addEventListener('keydown', (e) => {
|
|||
`
|
||||
return c.html(
|
||||
<Layout title={`${appName}/${filePath}`} editable>
|
||||
<PathBreadcrumb appName={appName} filePath={filePath} versionParam={versionParam} />
|
||||
<PathBreadcrumb appName={appName} filePath={filePath} />
|
||||
<CodeBlock>
|
||||
<CodeHeader>
|
||||
<span>{filename}</span>
|
||||
<div style="display:flex;align-items:center;gap:8px">
|
||||
<span id="save-status" style="font-size:12px;font-weight:normal;color:var(--colors-textMuted)"></span>
|
||||
<EditButton id="save-btn">Save</EditButton>
|
||||
<EditLink href={`/?app=${appName}${versionParam}&file=${filePath}`}>Done</EditLink>
|
||||
<EditLink href={`/?app=${appName}&file=${filePath}`}>Done</EditLink>
|
||||
</div>
|
||||
</CodeHeader>
|
||||
<pre id="editor" class={`language-${prismLang}`} contenteditable style="margin:0;padding:15px;min-height:300px;outline:none">{content}</pre>
|
||||
|
|
@ -660,11 +671,11 @@ document.addEventListener('keydown', (e) => {
|
|||
|
||||
return c.html(
|
||||
<Layout title={`${appName}/${filePath}`} highlight>
|
||||
<PathBreadcrumb appName={appName} filePath={filePath} versionParam={versionParam} />
|
||||
<PathBreadcrumb appName={appName} filePath={filePath} />
|
||||
<CodeBlock>
|
||||
<CodeHeader>
|
||||
<span>{filename}</span>
|
||||
<EditLink href={`/?app=${appName}${versionParam}&file=${filePath}&edit=1`}>Edit</EditLink>
|
||||
<EditLink href={`/?app=${appName}&file=${filePath}&edit=1`}>Edit</EditLink>
|
||||
</CodeHeader>
|
||||
<pre><code class={`language-${language}`}>{content}</code></pre>
|
||||
</CodeBlock>
|
||||
|
|
@ -676,11 +687,11 @@ document.addEventListener('keydown', (e) => {
|
|||
|
||||
return c.html(
|
||||
<Layout title={`${appName}${filePath ? `/${filePath}` : ''}`}>
|
||||
<PathBreadcrumb appName={appName} filePath={filePath} versionParam={versionParam} />
|
||||
<PathBreadcrumb appName={appName} filePath={filePath} />
|
||||
<FileList>
|
||||
{files.map(file => (
|
||||
<FileItem>
|
||||
<FileLink href={`/?app=${appName}${versionParam}&file=${file.path}`}>
|
||||
<FileLink href={`/?app=${appName}&file=${file.path}`}>
|
||||
{file.isDirectory ? <FolderIcon /> : <FileIconSvg />}
|
||||
<span>{file.name}</span>
|
||||
</FileLink>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user