@tinacms/cli 2.1.6 → 2.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +175 -43
- package/dist/next/commands/dev-command/server/media.d.ts +18 -1
- package/dist/next/database.d.ts +1 -1
- package/dist/next/vite/cors.d.ts +13 -0
- package/dist/next/vite/plugins.d.ts +2 -2
- package/dist/server/models/media.d.ts +17 -0
- package/dist/utils/path.d.ts +26 -0
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { Cli, Builtins } from "clipanion";
|
|
3
3
|
|
|
4
4
|
// package.json
|
|
5
|
-
var version = "2.1.
|
|
5
|
+
var version = "2.1.8";
|
|
6
6
|
|
|
7
7
|
// src/next/commands/dev-command/index.ts
|
|
8
8
|
import path8 from "path";
|
|
@@ -817,6 +817,14 @@ function stripNativeTrailingSlash(p) {
|
|
|
817
817
|
}
|
|
818
818
|
return str;
|
|
819
819
|
}
|
|
820
|
+
var PathTraversalError = class extends Error {
|
|
821
|
+
constructor(attemptedPath) {
|
|
822
|
+
super(
|
|
823
|
+
`Path traversal detected: the path "${attemptedPath}" escapes the allowed directory`
|
|
824
|
+
);
|
|
825
|
+
this.name = "PathTraversalError";
|
|
826
|
+
}
|
|
827
|
+
};
|
|
820
828
|
|
|
821
829
|
// src/next/config-manager.ts
|
|
822
830
|
var TINA_FOLDER = "tina";
|
|
@@ -1247,15 +1255,15 @@ var loaders = {
|
|
|
1247
1255
|
};
|
|
1248
1256
|
|
|
1249
1257
|
// src/next/database.ts
|
|
1258
|
+
import { createServer } from "net";
|
|
1250
1259
|
import {
|
|
1251
|
-
createDatabaseInternal,
|
|
1252
1260
|
FilesystemBridge,
|
|
1253
|
-
TinaLevelClient
|
|
1261
|
+
TinaLevelClient,
|
|
1262
|
+
createDatabaseInternal
|
|
1254
1263
|
} from "@tinacms/graphql";
|
|
1255
|
-
import { pipeline } from "readable-stream";
|
|
1256
|
-
import { createServer } from "net";
|
|
1257
1264
|
import { ManyLevelHost } from "many-level";
|
|
1258
1265
|
import { MemoryLevel } from "memory-level";
|
|
1266
|
+
import { pipeline } from "readable-stream";
|
|
1259
1267
|
var createDBServer = (port) => {
|
|
1260
1268
|
const levelHost = new ManyLevelHost(
|
|
1261
1269
|
// @ts-ignore
|
|
@@ -1274,7 +1282,7 @@ var createDBServer = (port) => {
|
|
|
1274
1282
|
);
|
|
1275
1283
|
}
|
|
1276
1284
|
});
|
|
1277
|
-
dbServer.listen(port);
|
|
1285
|
+
dbServer.listen(port, "localhost");
|
|
1278
1286
|
};
|
|
1279
1287
|
async function createAndInitializeDatabase(configManager, datalayerPort, bridgeOverride) {
|
|
1280
1288
|
let database;
|
|
@@ -1578,6 +1586,43 @@ import {
|
|
|
1578
1586
|
splitVendorChunkPlugin
|
|
1579
1587
|
} from "vite";
|
|
1580
1588
|
|
|
1589
|
+
// src/next/vite/cors.ts
|
|
1590
|
+
var LOCALHOST_RE = /^https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/;
|
|
1591
|
+
var PRIVATE_NETWORK_RE = /^https?:\/\/(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})(:\d+)?$/;
|
|
1592
|
+
function expandOrigins(raw) {
|
|
1593
|
+
const hasPrivate = raw.some((o) => o === "private");
|
|
1594
|
+
const filtered = raw.filter((o) => o !== "private");
|
|
1595
|
+
return hasPrivate ? [...filtered, PRIVATE_NETWORK_RE] : filtered;
|
|
1596
|
+
}
|
|
1597
|
+
function buildCorsOriginCheck(allowedOrigins = []) {
|
|
1598
|
+
const extra = expandOrigins(allowedOrigins);
|
|
1599
|
+
return (origin, callback) => {
|
|
1600
|
+
if (!origin) {
|
|
1601
|
+
callback(null, true);
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
if (LOCALHOST_RE.test(origin)) {
|
|
1605
|
+
callback(null, true);
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
for (const allowed of extra) {
|
|
1609
|
+
if (typeof allowed === "string") {
|
|
1610
|
+
if (allowed === origin) {
|
|
1611
|
+
callback(null, true);
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
} else {
|
|
1615
|
+
allowed.lastIndex = 0;
|
|
1616
|
+
if (allowed.test(origin)) {
|
|
1617
|
+
callback(null, true);
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
callback(null, false);
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1581
1626
|
// src/next/vite/filterPublicEnv.ts
|
|
1582
1627
|
function filterPublicEnv(env = process.env) {
|
|
1583
1628
|
const publicEnv = {};
|
|
@@ -2014,6 +2059,13 @@ var createConfig = async ({
|
|
|
2014
2059
|
},
|
|
2015
2060
|
server: {
|
|
2016
2061
|
host: configManager.config?.build?.host ?? false,
|
|
2062
|
+
// Restrict Vite's built-in CORS to the same origins our custom
|
|
2063
|
+
// middleware allows (localhost + user-configured allowedOrigins).
|
|
2064
|
+
cors: {
|
|
2065
|
+
origin: buildCorsOriginCheck(
|
|
2066
|
+
configManager.config?.server?.allowedOrigins
|
|
2067
|
+
)
|
|
2068
|
+
},
|
|
2017
2069
|
watch: noWatch ? {
|
|
2018
2070
|
ignored: ["**/*"]
|
|
2019
2071
|
} : {
|
|
@@ -2023,7 +2075,15 @@ var createConfig = async ({
|
|
|
2023
2075
|
]
|
|
2024
2076
|
},
|
|
2025
2077
|
fs: {
|
|
2026
|
-
strict:
|
|
2078
|
+
strict: true,
|
|
2079
|
+
// Allow serving files from the project root and the SPA package.
|
|
2080
|
+
// Without this, Vite would block access to tina config/generated
|
|
2081
|
+
// files since the Vite root is the @tinacms/app package directory.
|
|
2082
|
+
allow: [
|
|
2083
|
+
configManager.spaRootPath,
|
|
2084
|
+
configManager.rootPath,
|
|
2085
|
+
...configManager.contentRootPath && configManager.contentRootPath !== configManager.rootPath ? [configManager.contentRootPath] : []
|
|
2086
|
+
]
|
|
2027
2087
|
}
|
|
2028
2088
|
},
|
|
2029
2089
|
build: {
|
|
@@ -2053,19 +2113,19 @@ var createConfig = async ({
|
|
|
2053
2113
|
};
|
|
2054
2114
|
|
|
2055
2115
|
// src/next/vite/plugins.ts
|
|
2056
|
-
import { createFilter } from "@rollup/pluginutils";
|
|
2057
2116
|
import fs6 from "fs";
|
|
2058
|
-
import { transformWithEsbuild } from "vite";
|
|
2059
|
-
import { transform as esbuildTransform } from "esbuild";
|
|
2060
2117
|
import path7 from "path";
|
|
2118
|
+
import { createFilter } from "@rollup/pluginutils";
|
|
2119
|
+
import { resolve as gqlResolve } from "@tinacms/graphql";
|
|
2061
2120
|
import bodyParser from "body-parser";
|
|
2062
2121
|
import cors from "cors";
|
|
2063
|
-
import {
|
|
2122
|
+
import { transform as esbuildTransform } from "esbuild";
|
|
2123
|
+
import { transformWithEsbuild } from "vite";
|
|
2064
2124
|
|
|
2065
2125
|
// src/next/commands/dev-command/server/media.ts
|
|
2066
|
-
import fs5 from "fs-extra";
|
|
2067
2126
|
import path6, { join } from "path";
|
|
2068
2127
|
import busboy from "busboy";
|
|
2128
|
+
import fs5 from "fs-extra";
|
|
2069
2129
|
var createMediaRouter = (config2) => {
|
|
2070
2130
|
const mediaFolder = path6.join(
|
|
2071
2131
|
config2.rootPath,
|
|
@@ -2074,31 +2134,68 @@ var createMediaRouter = (config2) => {
|
|
|
2074
2134
|
);
|
|
2075
2135
|
const mediaModel = new MediaModel(config2);
|
|
2076
2136
|
const handleList = async (req, res) => {
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
cursor
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2137
|
+
try {
|
|
2138
|
+
const requestURL = new URL(req.url, config2.apiURL);
|
|
2139
|
+
const folder = decodeURIComponent(
|
|
2140
|
+
requestURL.pathname.replace("/media/list/", "")
|
|
2141
|
+
);
|
|
2142
|
+
const limit = requestURL.searchParams.get("limit");
|
|
2143
|
+
const cursor = requestURL.searchParams.get("cursor");
|
|
2144
|
+
const media = await mediaModel.listMedia({
|
|
2145
|
+
searchPath: folder,
|
|
2146
|
+
cursor,
|
|
2147
|
+
limit
|
|
2148
|
+
});
|
|
2149
|
+
res.end(JSON.stringify(media));
|
|
2150
|
+
} catch (error) {
|
|
2151
|
+
if (error instanceof PathTraversalError) {
|
|
2152
|
+
res.statusCode = 403;
|
|
2153
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
2154
|
+
return;
|
|
2155
|
+
}
|
|
2156
|
+
throw error;
|
|
2157
|
+
}
|
|
2087
2158
|
};
|
|
2088
2159
|
const handleDelete = async (req, res) => {
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2160
|
+
try {
|
|
2161
|
+
const file = decodeURIComponent(req.url.slice("/media/".length));
|
|
2162
|
+
const didDelete = await mediaModel.deleteMedia({ searchPath: file });
|
|
2163
|
+
res.end(JSON.stringify(didDelete));
|
|
2164
|
+
} catch (error) {
|
|
2165
|
+
if (error instanceof PathTraversalError) {
|
|
2166
|
+
res.statusCode = 403;
|
|
2167
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
2168
|
+
return;
|
|
2169
|
+
}
|
|
2170
|
+
throw error;
|
|
2171
|
+
}
|
|
2092
2172
|
};
|
|
2093
2173
|
const handlePost = async function(req, res) {
|
|
2094
2174
|
const bb = busboy({ headers: req.headers });
|
|
2175
|
+
let responded = false;
|
|
2095
2176
|
bb.on("file", async (_name, file, _info) => {
|
|
2096
|
-
const fullPath =
|
|
2097
|
-
|
|
2177
|
+
const fullPath = decodeURIComponent(
|
|
2178
|
+
req.url?.slice("/media/upload/".length)
|
|
2179
|
+
);
|
|
2180
|
+
let saveTo;
|
|
2181
|
+
try {
|
|
2182
|
+
saveTo = resolveStrictlyWithinBase(fullPath, mediaFolder);
|
|
2183
|
+
} catch {
|
|
2184
|
+
responded = true;
|
|
2185
|
+
file.resume();
|
|
2186
|
+
res.statusCode = 403;
|
|
2187
|
+
res.end(
|
|
2188
|
+
JSON.stringify({
|
|
2189
|
+
error: `Path traversal detected: ${fullPath}`
|
|
2190
|
+
})
|
|
2191
|
+
);
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
2098
2194
|
await fs5.ensureDir(path6.dirname(saveTo));
|
|
2099
2195
|
file.pipe(fs5.createWriteStream(saveTo));
|
|
2100
2196
|
});
|
|
2101
2197
|
bb.on("error", (error) => {
|
|
2198
|
+
responded = true;
|
|
2102
2199
|
res.statusCode = 500;
|
|
2103
2200
|
if (error instanceof Error) {
|
|
2104
2201
|
res.end(JSON.stringify({ message: error }));
|
|
@@ -2107,6 +2204,7 @@ var createMediaRouter = (config2) => {
|
|
|
2107
2204
|
}
|
|
2108
2205
|
});
|
|
2109
2206
|
bb.on("close", () => {
|
|
2207
|
+
if (responded) return;
|
|
2110
2208
|
res.statusCode = 200;
|
|
2111
2209
|
res.end(JSON.stringify({ success: true }));
|
|
2112
2210
|
});
|
|
@@ -2121,6 +2219,32 @@ var parseMediaFolder = (str) => {
|
|
|
2121
2219
|
returnString = returnString.substr(0, returnString.length - 1);
|
|
2122
2220
|
return returnString;
|
|
2123
2221
|
};
|
|
2222
|
+
var ENCODED_TRAVERSAL_RE = /%2e%2e|%2f|%5c/i;
|
|
2223
|
+
function resolveWithinBase(userPath, baseDir) {
|
|
2224
|
+
if (ENCODED_TRAVERSAL_RE.test(userPath)) {
|
|
2225
|
+
throw new PathTraversalError(userPath);
|
|
2226
|
+
}
|
|
2227
|
+
const resolvedBase = path6.resolve(baseDir);
|
|
2228
|
+
const resolved = path6.resolve(path6.join(baseDir, userPath));
|
|
2229
|
+
if (resolved === resolvedBase) {
|
|
2230
|
+
return resolvedBase;
|
|
2231
|
+
}
|
|
2232
|
+
if (resolved.startsWith(resolvedBase + path6.sep)) {
|
|
2233
|
+
return resolved;
|
|
2234
|
+
}
|
|
2235
|
+
throw new PathTraversalError(userPath);
|
|
2236
|
+
}
|
|
2237
|
+
function resolveStrictlyWithinBase(userPath, baseDir) {
|
|
2238
|
+
if (ENCODED_TRAVERSAL_RE.test(userPath)) {
|
|
2239
|
+
throw new PathTraversalError(userPath);
|
|
2240
|
+
}
|
|
2241
|
+
const resolvedBase = path6.resolve(baseDir) + path6.sep;
|
|
2242
|
+
const resolved = path6.resolve(path6.join(baseDir, userPath));
|
|
2243
|
+
if (!resolved.startsWith(resolvedBase)) {
|
|
2244
|
+
throw new PathTraversalError(userPath);
|
|
2245
|
+
}
|
|
2246
|
+
return resolved;
|
|
2247
|
+
}
|
|
2124
2248
|
var MediaModel = class {
|
|
2125
2249
|
rootPath;
|
|
2126
2250
|
publicFolder;
|
|
@@ -2132,22 +2256,18 @@ var MediaModel = class {
|
|
|
2132
2256
|
}
|
|
2133
2257
|
async listMedia(args) {
|
|
2134
2258
|
try {
|
|
2135
|
-
const
|
|
2136
|
-
|
|
2137
|
-
this.publicFolder,
|
|
2138
|
-
this.mediaRoot,
|
|
2139
|
-
decodeURIComponent(args.searchPath)
|
|
2140
|
-
);
|
|
2259
|
+
const mediaBase = join(this.rootPath, this.publicFolder, this.mediaRoot);
|
|
2260
|
+
const validatedPath = resolveWithinBase(args.searchPath, mediaBase);
|
|
2141
2261
|
const searchPath = parseMediaFolder(args.searchPath);
|
|
2142
|
-
if (!await fs5.pathExists(
|
|
2262
|
+
if (!await fs5.pathExists(validatedPath)) {
|
|
2143
2263
|
return {
|
|
2144
2264
|
files: [],
|
|
2145
2265
|
directories: []
|
|
2146
2266
|
};
|
|
2147
2267
|
}
|
|
2148
|
-
const filesStr = await fs5.readdir(
|
|
2268
|
+
const filesStr = await fs5.readdir(validatedPath);
|
|
2149
2269
|
const filesProm = filesStr.map(async (file) => {
|
|
2150
|
-
const filePath = join(
|
|
2270
|
+
const filePath = join(validatedPath, file);
|
|
2151
2271
|
const stat = await fs5.stat(filePath);
|
|
2152
2272
|
let src = `/${file}`;
|
|
2153
2273
|
const isFile = stat.isFile();
|
|
@@ -2194,6 +2314,7 @@ var MediaModel = class {
|
|
|
2194
2314
|
cursor
|
|
2195
2315
|
};
|
|
2196
2316
|
} catch (error) {
|
|
2317
|
+
if (error instanceof PathTraversalError) throw error;
|
|
2197
2318
|
console.error(error);
|
|
2198
2319
|
return {
|
|
2199
2320
|
files: [],
|
|
@@ -2204,16 +2325,13 @@ var MediaModel = class {
|
|
|
2204
2325
|
}
|
|
2205
2326
|
async deleteMedia(args) {
|
|
2206
2327
|
try {
|
|
2207
|
-
const
|
|
2208
|
-
|
|
2209
|
-
this.publicFolder,
|
|
2210
|
-
this.mediaRoot,
|
|
2211
|
-
decodeURIComponent(args.searchPath)
|
|
2212
|
-
);
|
|
2328
|
+
const mediaBase = join(this.rootPath, this.publicFolder, this.mediaRoot);
|
|
2329
|
+
const file = resolveStrictlyWithinBase(args.searchPath, mediaBase);
|
|
2213
2330
|
await fs5.stat(file);
|
|
2214
2331
|
await fs5.remove(file);
|
|
2215
2332
|
return { ok: true };
|
|
2216
2333
|
} catch (error) {
|
|
2334
|
+
if (error instanceof PathTraversalError) throw error;
|
|
2217
2335
|
console.error(error);
|
|
2218
2336
|
return { ok: false, message: error?.toString() };
|
|
2219
2337
|
}
|
|
@@ -2336,10 +2454,18 @@ var devServerEndPointsPlugin = ({
|
|
|
2336
2454
|
searchIndex,
|
|
2337
2455
|
databaseLock
|
|
2338
2456
|
}) => {
|
|
2457
|
+
const corsOriginCheck = buildCorsOriginCheck(
|
|
2458
|
+
configManager.config?.server?.allowedOrigins
|
|
2459
|
+
);
|
|
2339
2460
|
const plug = {
|
|
2340
2461
|
name: "graphql-endpoints",
|
|
2341
2462
|
configureServer(server) {
|
|
2342
|
-
server.middlewares.use(
|
|
2463
|
+
server.middlewares.use(
|
|
2464
|
+
cors({
|
|
2465
|
+
origin: corsOriginCheck,
|
|
2466
|
+
methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"]
|
|
2467
|
+
})
|
|
2468
|
+
);
|
|
2343
2469
|
server.middlewares.use(bodyParser.json({ limit: "5mb" }));
|
|
2344
2470
|
server.middlewares.use(async (req, res, next) => {
|
|
2345
2471
|
const mediaPaths = configManager.config.media?.tina;
|
|
@@ -5806,6 +5932,12 @@ import { LocalAuthProvider } from "tinacms";`;
|
|
|
5806
5932
|
outputFolder: "admin",
|
|
5807
5933
|
publicFolder: "${args.publicFolder}",
|
|
5808
5934
|
},
|
|
5935
|
+
// Uncomment to allow cross-origin requests from non-localhost origins
|
|
5936
|
+
// during local development (e.g. GitHub Codespaces, Gitpod, Docker).
|
|
5937
|
+
// Use 'private' to allow all private-network IPs (WSL2, Docker, etc.)
|
|
5938
|
+
// server: {
|
|
5939
|
+
// allowedOrigins: ['https://your-codespace.github.dev'],
|
|
5940
|
+
// },
|
|
5809
5941
|
media: {
|
|
5810
5942
|
tina: {
|
|
5811
5943
|
mediaRoot: "",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Connect } from 'vite';
|
|
2
1
|
import type { ServerResponse } from 'http';
|
|
2
|
+
import type { Connect } from 'vite';
|
|
3
3
|
export declare const createMediaRouter: (config: PathConfig) => {
|
|
4
4
|
handleList: (req: any, res: any) => Promise<void>;
|
|
5
5
|
handleDelete: (req: Connect.IncomingMessage, res: any) => Promise<void>;
|
|
@@ -34,6 +34,23 @@ type SuccessRecord = {
|
|
|
34
34
|
ok: false;
|
|
35
35
|
message: string;
|
|
36
36
|
};
|
|
37
|
+
/**
|
|
38
|
+
* Handles media file operations (list, delete) for the Vite-based dev server.
|
|
39
|
+
*
|
|
40
|
+
* @security Every method that accepts a user-supplied `searchPath` validates
|
|
41
|
+
* it against the media root using `resolveWithinBase` (list) or
|
|
42
|
+
* `resolveStrictlyWithinBase` (delete) before any filesystem access.
|
|
43
|
+
*
|
|
44
|
+
* - **list** uses `resolveWithinBase` because listing the media root itself
|
|
45
|
+
* (empty path / exact base match) is a valid operation.
|
|
46
|
+
* - **delete** uses `resolveStrictlyWithinBase` because deleting the media
|
|
47
|
+
* root directory itself must never be allowed.
|
|
48
|
+
*
|
|
49
|
+
* Both methods catch `PathTraversalError` and re-throw it so that the
|
|
50
|
+
* route handler can return a 403 response. Other errors are caught and
|
|
51
|
+
* returned as structured error responses (this avoids leaking stack traces
|
|
52
|
+
* to the client).
|
|
53
|
+
*/
|
|
37
54
|
export declare class MediaModel {
|
|
38
55
|
readonly rootPath: string;
|
|
39
56
|
readonly publicFolder: string;
|
package/dist/next/database.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Bridge, Database } from '@tinacms/graphql';
|
|
2
2
|
import { ConfigManager } from './config-manager';
|
|
3
3
|
export declare const createDBServer: (port: number) => void;
|
|
4
4
|
export declare function createAndInitializeDatabase(configManager: ConfigManager, datalayerPort: number, bridgeOverride?: Bridge): Promise<Database>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared CORS origin-checking logic for the TinaCMS dev server.
|
|
3
|
+
*
|
|
4
|
+
* By default only localhost / 127.0.0.1 / [::1] (any port) are allowed.
|
|
5
|
+
* Users can extend this via `server.allowedOrigins` in their tina config.
|
|
6
|
+
* The special keyword `'private'` expands to all RFC 1918 private-network
|
|
7
|
+
* IP ranges (10.x, 172.16-31.x, 192.168.x) — useful for WSL2, Docker
|
|
8
|
+
* bridge networks, etc.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Build a CORS `origin` callback compatible with the `cors` npm package.
|
|
12
|
+
*/
|
|
13
|
+
export declare function buildCorsOriginCheck(allowedOrigins?: (string | RegExp)[]): (origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void) => void;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { Plugin } from 'vite';
|
|
2
1
|
import { FilterPattern } from '@rollup/pluginutils';
|
|
3
2
|
import type { Config } from '@svgr/core';
|
|
4
|
-
import { transformWithEsbuild } from 'vite';
|
|
5
3
|
import type { Database } from '@tinacms/graphql';
|
|
4
|
+
import type { Plugin } from 'vite';
|
|
5
|
+
import { transformWithEsbuild } from 'vite';
|
|
6
6
|
import type { ConfigManager } from '../config-manager';
|
|
7
7
|
export declare const transformTsxPlugin: ({ configManager: _configManager, }: {
|
|
8
8
|
configManager: ConfigManager;
|
|
@@ -28,6 +28,23 @@ type SuccessRecord = {
|
|
|
28
28
|
ok: false;
|
|
29
29
|
message: string;
|
|
30
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* Handles media file operations (list, delete) for the Express-based server.
|
|
33
|
+
*
|
|
34
|
+
* @security Every method that accepts a user-supplied `searchPath` validates
|
|
35
|
+
* it against the media root using `resolveWithinBase` (list) or
|
|
36
|
+
* `resolveStrictlyWithinBase` (delete) before any filesystem access.
|
|
37
|
+
*
|
|
38
|
+
* - **list** uses `resolveWithinBase` because listing the media root itself
|
|
39
|
+
* (empty path / exact base match) is a valid operation.
|
|
40
|
+
* - **delete** uses `resolveStrictlyWithinBase` because deleting the media
|
|
41
|
+
* root directory itself must never be allowed.
|
|
42
|
+
*
|
|
43
|
+
* Both methods catch `PathTraversalError` and re-throw it so that the
|
|
44
|
+
* route handler can return a 403 response. Other errors are caught and
|
|
45
|
+
* returned as structured error responses (this avoids leaking stack traces
|
|
46
|
+
* to the client).
|
|
47
|
+
*/
|
|
31
48
|
export declare class MediaModel {
|
|
32
49
|
readonly rootPath: string;
|
|
33
50
|
readonly publicFolder: string;
|
package/dist/utils/path.d.ts
CHANGED
|
@@ -1,3 +1,29 @@
|
|
|
1
1
|
/** Removes trailing slash from path. Separator to remove is chosen based on
|
|
2
2
|
* operating system. */
|
|
3
3
|
export declare function stripNativeTrailingSlash(p: string): string;
|
|
4
|
+
/**
|
|
5
|
+
* Validates that a user-supplied path does not escape the base directory
|
|
6
|
+
* via path traversal (CWE-22). Returns the resolved absolute path.
|
|
7
|
+
*
|
|
8
|
+
* Allows an exact base match (empty or `.` input) — use this for list/read
|
|
9
|
+
* operations where referencing the root directory itself is valid. For
|
|
10
|
+
* delete/write operations where you need to target an actual file, use the
|
|
11
|
+
* `resolveStrictlyWithinBase` variant (currently inlined in media models).
|
|
12
|
+
*
|
|
13
|
+
* As a safety net, also rejects paths that still contain URL-encoded
|
|
14
|
+
* traversal sequences (`%2e%2e`, `%2f`, `%5c`), catching cases where the
|
|
15
|
+
* caller forgot to decode.
|
|
16
|
+
*
|
|
17
|
+
* @security This is the canonical implementation. Inlined copies exist in
|
|
18
|
+
* the media model files for CodeQL compatibility — keep them in sync.
|
|
19
|
+
*
|
|
20
|
+
* @param userPath - The untrusted path from the request (must already be
|
|
21
|
+
* URI-decoded by the caller).
|
|
22
|
+
* @param baseDir - The trusted base directory the path must stay within.
|
|
23
|
+
* @returns The resolved absolute path.
|
|
24
|
+
* @throws {PathTraversalError} If the path escapes the base directory.
|
|
25
|
+
*/
|
|
26
|
+
export declare function assertPathWithinBase(userPath: string, baseDir: string): string;
|
|
27
|
+
export declare class PathTraversalError extends Error {
|
|
28
|
+
constructor(attemptedPath: string);
|
|
29
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tinacms/cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.1.
|
|
4
|
+
"version": "2.1.8",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"typings": "dist/index.d.ts",
|
|
7
7
|
"files": [
|
|
@@ -88,12 +88,12 @@
|
|
|
88
88
|
"vite": "^4.5.9",
|
|
89
89
|
"yup": "^1.6.1",
|
|
90
90
|
"zod": "^3.24.2",
|
|
91
|
-
"@tinacms/app": "2.3.
|
|
92
|
-
"@tinacms/
|
|
93
|
-
"@tinacms/graphql": "2.1.2",
|
|
91
|
+
"@tinacms/app": "2.3.27",
|
|
92
|
+
"@tinacms/graphql": "2.1.4",
|
|
94
93
|
"@tinacms/metrics": "2.0.1",
|
|
95
|
-
"@tinacms/
|
|
96
|
-
"tinacms": "
|
|
94
|
+
"@tinacms/schema-tools": "2.7.0",
|
|
95
|
+
"@tinacms/search": "1.2.5",
|
|
96
|
+
"tinacms": "3.6.0"
|
|
97
97
|
},
|
|
98
98
|
"publishConfig": {
|
|
99
99
|
"registry": "https://registry.npmjs.org"
|