mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-02 12:51:57 +00:00
fix(tlon): address security review issues
- Fix SSRF in upload.ts: use urbitFetch with SSRF protection - Fix SSRF in media.ts: use urbitFetch with SSRF protection - Add command whitelist to tlon tool to prevent command injection - Add getDefaultSsrFPolicy() helper for uploads/downloads
This commit is contained in:
committed by
Josh Lehman
parent
c629b27d97
commit
01715182e1
@@ -9,6 +9,21 @@ import { setTlonRuntime } from "./src/runtime.js";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// Whitelist of allowed tlon subcommands
|
||||
const ALLOWED_TLON_COMMANDS = new Set([
|
||||
"activity",
|
||||
"channels",
|
||||
"contacts",
|
||||
"groups",
|
||||
"messages",
|
||||
"dms",
|
||||
"posts",
|
||||
"notebook",
|
||||
"settings",
|
||||
"help",
|
||||
"version",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Find the tlon binary from the skill package
|
||||
*/
|
||||
@@ -141,6 +156,21 @@ const plugin = {
|
||||
async execute(_id: string, params: { command: string }) {
|
||||
try {
|
||||
const args = shellSplit(params.command);
|
||||
|
||||
// Validate first argument is a whitelisted tlon subcommand
|
||||
const subcommand = args[0];
|
||||
if (!ALLOWED_TLON_COMMANDS.has(subcommand)) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: `Error: Unknown tlon subcommand '${subcommand}'. Allowed: ${[...ALLOWED_TLON_COMMANDS].join(", ")}`,
|
||||
},
|
||||
],
|
||||
details: { error: true },
|
||||
};
|
||||
}
|
||||
|
||||
const output = await runTlonCommand(tlonBinary, args);
|
||||
return {
|
||||
content: [{ type: "text" as const, text: output }],
|
||||
|
||||
@@ -5,6 +5,8 @@ import { homedir } from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { Readable } from "node:stream";
|
||||
import { pipeline } from "node:stream/promises";
|
||||
import { getDefaultSsrFPolicy } from "../urbit/context.js";
|
||||
import { urbitFetch } from "../urbit/fetch.js";
|
||||
|
||||
// Default to OpenClaw workspace media directory
|
||||
const DEFAULT_MEDIA_DIR = path.join(homedir(), ".openclaw", "workspace", "media", "inbound");
|
||||
@@ -52,11 +54,25 @@ export async function downloadMedia(
|
||||
mediaDir: string = DEFAULT_MEDIA_DIR,
|
||||
): Promise<DownloadedMedia | null> {
|
||||
try {
|
||||
// Validate URL is http/https before fetching
|
||||
const parsedUrl = new URL(url);
|
||||
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
|
||||
console.warn(`[tlon-media] Rejected non-http(s) URL: ${url}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ensure media directory exists
|
||||
await mkdir(mediaDir, { recursive: true });
|
||||
|
||||
// Fetch the image
|
||||
const response = await fetch(url);
|
||||
// Fetch with SSRF protection
|
||||
const { response } = await urbitFetch({
|
||||
baseUrl: url,
|
||||
path: "",
|
||||
init: { method: "GET" },
|
||||
ssrfPolicy: getDefaultSsrFPolicy(),
|
||||
auditContext: "tlon-media-download",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`[tlon-media] Failed to fetch ${url}: ${response.status}`);
|
||||
return null;
|
||||
|
||||
@@ -45,3 +45,12 @@ export function ssrfPolicyFromAllowPrivateNetwork(
|
||||
): SsrFPolicy | undefined {
|
||||
return allowPrivateNetwork ? { allowPrivateNetwork: true } : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default SSRF policy for image uploads.
|
||||
* Uses a restrictive policy that blocks private networks by default.
|
||||
*/
|
||||
export function getDefaultSsrFPolicy(): SsrFPolicy | undefined {
|
||||
// Default: block private networks for image uploads (safer default)
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
* Upload an image from a URL to Tlon storage.
|
||||
*/
|
||||
import { uploadFile } from "@tloncorp/api";
|
||||
import { getDefaultSsrFPolicy } from "./context.js";
|
||||
import { urbitFetch } from "./fetch.js";
|
||||
|
||||
/**
|
||||
* Fetch an image from a URL and upload it to Tlon storage.
|
||||
@@ -11,8 +13,22 @@ import { uploadFile } from "@tloncorp/api";
|
||||
*/
|
||||
export async function uploadImageFromUrl(imageUrl: string): Promise<string> {
|
||||
try {
|
||||
// Fetch the image
|
||||
const response = await fetch(imageUrl);
|
||||
// Validate URL is http/https before fetching
|
||||
const url = new URL(imageUrl);
|
||||
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
||||
console.warn(`[tlon] Rejected non-http(s) URL: ${imageUrl}`);
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
// Fetch the image with SSRF protection
|
||||
const { response } = await urbitFetch({
|
||||
baseUrl: imageUrl,
|
||||
path: "",
|
||||
init: { method: "GET" },
|
||||
ssrfPolicy: getDefaultSsrFPolicy(),
|
||||
auditContext: "tlon-upload-image",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn(`[tlon] Failed to fetch image from ${imageUrl}: ${response.status}`);
|
||||
return imageUrl;
|
||||
|
||||
Reference in New Issue
Block a user