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:
Hunter Miller
2026-02-19 14:33:44 -06:00
committed by Josh Lehman
parent c629b27d97
commit 01715182e1
4 changed files with 75 additions and 4 deletions

View File

@@ -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 }],

View File

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

View File

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

View File

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