mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(codeql): harden release alert sinks (#4554)
This commit is contained in:
@@ -147,6 +147,11 @@ type ReplyMode = 'text' | 'voice';
|
||||
const AUTOCOMPLETE_POLL_DEBOUNCE_MS = 320;
|
||||
const AUTOCOMPLETE_MIN_CONTEXT_CHARS = 3;
|
||||
const debug = debugFactory('conversations');
|
||||
const SAFE_IMAGE_DATA_URI_RE = /^data:image\/(?:png|jpe?g|gif|webp|bmp);base64,[a-z0-9+/=\s]+$/i;
|
||||
|
||||
function isSafeAttachmentImageSrc(src: string): boolean {
|
||||
return SAFE_IMAGE_DATA_URI_RE.test(src);
|
||||
}
|
||||
|
||||
interface ConversationsProps {
|
||||
/**
|
||||
@@ -2321,9 +2326,11 @@ const Conversations = ({
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
{(() => {
|
||||
const displayText = parsedContent.text;
|
||||
const dataUris = Array.isArray(msg.extraMetadata?.attachmentDataUris)
|
||||
? (msg.extraMetadata.attachmentDataUris as string[])
|
||||
: parsedContent.dataUris;
|
||||
const dataUris = (
|
||||
Array.isArray(msg.extraMetadata?.attachmentDataUris)
|
||||
? (msg.extraMetadata.attachmentDataUris as string[])
|
||||
: parsedContent.dataUris
|
||||
).filter(isSafeAttachmentImageSrc);
|
||||
const hasImages = dataUris.length > 0;
|
||||
// Document attachments carry no image data-URI (only
|
||||
// images do); surface them as filename chips from the
|
||||
|
||||
@@ -114,10 +114,18 @@ describe('Jira Composio connector flow', () => {
|
||||
return (
|
||||
document.querySelector('[data-testid="composio-required-subdomain"]') !== null ||
|
||||
document.querySelector('input[placeholder*="subdomain"]') !== null ||
|
||||
// fallback: any .atlassian.net suffix label
|
||||
Array.from(document.querySelectorAll('*')).some(el =>
|
||||
(el.textContent ?? '').includes('.atlassian.net')
|
||||
)
|
||||
// fallback: any visible Jira tenant URL label
|
||||
Array.from(document.querySelectorAll('*')).some(el => {
|
||||
const text = el.textContent ?? '';
|
||||
const matches = text.match(/https?:\/\/[^\s"'<>]+/g) ?? [];
|
||||
return matches.some(candidate => {
|
||||
try {
|
||||
return new URL(candidate).hostname.endsWith('.atlassian.net');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch(() => false);
|
||||
|
||||
@@ -721,9 +721,14 @@ describe('Mega flow — login + Gmail OAuth + Composio in one session', () => {
|
||||
console.log(`${LOG} update.version: asset_prefix = ${prefix}`);
|
||||
|
||||
// No outbound HTTP call should have been made — version is purely local.
|
||||
const outbound = getRequestLog().find(
|
||||
r => r.url.includes('github.com') || r.url.includes('update.openhuman.app')
|
||||
);
|
||||
const outbound = getRequestLog().find(r => {
|
||||
try {
|
||||
const url = new URL(r.url, 'http://mock.local');
|
||||
return url.hostname === 'github.com' || url.hostname === 'update.openhuman.app';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
expect(outbound).toBeUndefined();
|
||||
|
||||
const ping = await callOpenhumanRpc('core.ping', {});
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
import { buildDynamicCompletion } from "./llm/dynamic.mjs";
|
||||
import { headerValue, pickProbeText, resolveThreadKey } from "./llm/shared.mjs";
|
||||
|
||||
const MAX_STREAM_DELAY_MS = 30_000;
|
||||
|
||||
// The scripted `llmForcedResponses` FIFO models the *interactive* agent turn,
|
||||
// which always advertises tools (the orchestrator's delegate_* tools). Ancillary
|
||||
// completions that share the endpoint but carry no tools — thread-title/summary
|
||||
@@ -175,6 +177,12 @@ function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function safeDelayMs(raw, fallback = 0) {
|
||||
const parsed = Number(raw);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
||||
return Math.min(parsed, MAX_STREAM_DELAY_MS);
|
||||
}
|
||||
|
||||
// Split a string into N-character windows so we can stream tool-call
|
||||
// argument JSON the same way real providers do — clients accumulate the
|
||||
// partial fragments and JSON-parse at the end.
|
||||
@@ -312,11 +320,7 @@ function handleStreamingCompletion({
|
||||
}
|
||||
}
|
||||
|
||||
const defaultDelayMs = Number.isFinite(
|
||||
parseFloat(mockBehavior.llmStreamChunkDelayMs),
|
||||
)
|
||||
? Math.max(0, parseFloat(mockBehavior.llmStreamChunkDelayMs))
|
||||
: 25;
|
||||
const defaultDelayMs = safeDelayMs(mockBehavior.llmStreamChunkDelayMs, 25);
|
||||
|
||||
// Fire-and-forget — the dispatcher only cares that the handler
|
||||
// claimed the request. Errors mid-stream are surfaced through SSE.
|
||||
@@ -344,9 +348,7 @@ async function streamScriptToResponse({ res, model, script, defaultDelayMs }) {
|
||||
let trailingUsage = null;
|
||||
for (let i = 0; i < script.length; i += 1) {
|
||||
const entry = script[i] ?? {};
|
||||
const delay = Number.isFinite(entry.delayMs)
|
||||
? entry.delayMs
|
||||
: defaultDelayMs;
|
||||
const delay = safeDelayMs(entry.delayMs, defaultDelayMs);
|
||||
if (delay > 0) await sleep(delay);
|
||||
|
||||
if (entry.error) {
|
||||
|
||||
@@ -41,7 +41,8 @@ export function handleOAuth(ctx) {
|
||||
const legacy = url.match(
|
||||
/^\/mock-(telegram|notion|google|gmail|slack|discord|twitter|github)-oauth\/?(\?.*)?$/i,
|
||||
);
|
||||
const legacyGeneric = !newStyle && !legacy && /^\/mock-oauth\/?(\?.*)?$/.test(url);
|
||||
const legacyGeneric =
|
||||
!newStyle && !legacy && /^\/mock-oauth\/?(\?.*)?$/.test(url);
|
||||
|
||||
if (method === "GET" && (newStyle || legacy || legacyGeneric)) {
|
||||
const provider = newStyle?.[1] || legacy?.[1] || "generic";
|
||||
@@ -70,14 +71,20 @@ export function handleOAuth(ctx) {
|
||||
integrationId,
|
||||
)}&provider=${encodeURIComponent(params.provider || provider)}`;
|
||||
|
||||
html(res, 200, renderOAuthPage({ provider, target, autoRedirectMs, errorCode }));
|
||||
html(
|
||||
res,
|
||||
200,
|
||||
renderOAuthPage({ provider, target, autoRedirectMs, errorCode }),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Generic callback exchange. Real providers each hit their own
|
||||
// backend-specific URL; for e2e a single endpoint per provider that
|
||||
// always returns a session token is enough.
|
||||
const callbackMatch = url.match(/^\/auth\/([a-z][a-z0-9_-]*)\/callback\/?(\?.*)?$/i);
|
||||
const callbackMatch = url.match(
|
||||
/^\/auth\/([a-z][a-z0-9_-]*)\/callback\/?(\?.*)?$/i,
|
||||
);
|
||||
if (method === "GET" && callbackMatch) {
|
||||
const provider = callbackMatch[1];
|
||||
const params = parseQuery(url);
|
||||
@@ -156,7 +163,9 @@ function parseQuery(url) {
|
||||
const key = eq < 0 ? pair : pair.slice(0, eq);
|
||||
const raw = eq < 0 ? "" : pair.slice(eq + 1);
|
||||
try {
|
||||
out[decodeURIComponent(key)] = decodeURIComponent(raw.replace(/\+/g, " "));
|
||||
out[decodeURIComponent(key)] = decodeURIComponent(
|
||||
raw.replace(/\+/g, " "),
|
||||
);
|
||||
} catch {
|
||||
out[key] = raw;
|
||||
}
|
||||
@@ -171,13 +180,17 @@ function clampDelay(raw, fallback) {
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s).replace(/[&<>"']/g, (c) => ({
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
})[c]);
|
||||
return String(s).replace(
|
||||
/[&<>"']/g,
|
||||
(c) =>
|
||||
({
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
})[c],
|
||||
);
|
||||
}
|
||||
|
||||
function renderOAuthPage({ provider, target, autoRedirectMs, errorCode }) {
|
||||
@@ -193,15 +206,22 @@ function renderOAuthPage({ provider, target, autoRedirectMs, errorCode }) {
|
||||
const autoRedirectScript =
|
||||
autoRedirectMs === null
|
||||
? ""
|
||||
: `<script>setTimeout(function(){window.location.href=${JSON.stringify(
|
||||
target,
|
||||
)};}, ${Number(autoRedirectMs)});</script>`;
|
||||
: `<script>
|
||||
(function () {
|
||||
var delay = Number(document.body.dataset.autoRedirectMs || 0);
|
||||
window.setTimeout(function () {
|
||||
var link = document.getElementById("continue");
|
||||
if (link instanceof HTMLAnchorElement) window.location.href = link.href;
|
||||
}, Number.isFinite(delay) && delay >= 0 ? delay : 0);
|
||||
})();
|
||||
</script>`;
|
||||
|
||||
const metaRefresh =
|
||||
autoRedirectMs === null
|
||||
? ""
|
||||
: `<meta http-equiv="refresh" content="${(Number(autoRedirectMs) /
|
||||
1000).toFixed(2)};url=${safeTarget}" />`;
|
||||
: `<meta http-equiv="refresh" content="${(
|
||||
Number(autoRedirectMs) / 1000
|
||||
).toFixed(2)};url=${safeTarget}" />`;
|
||||
|
||||
return `<!doctype html>
|
||||
<html lang="en">
|
||||
@@ -217,7 +237,7 @@ function renderOAuthPage({ provider, target, autoRedirectMs, errorCode }) {
|
||||
code { background: #f3f3f3; padding: 2px 6px; border-radius: 4px; font-size: 13px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<body${autoRedirectMs === null ? "" : ` data-auto-redirect-ms="${Number(autoRedirectMs)}"`}>
|
||||
<h1>${heading}</h1>
|
||||
<p>${blurb}</p>
|
||||
<p>Target: <code>${safeTarget}</code></p>
|
||||
|
||||
@@ -124,12 +124,7 @@ function ruleMatches(rule, ctx) {
|
||||
return false;
|
||||
if (typeof rule.path === "string" && rule.path !== ctx.url) return false;
|
||||
if (typeof rule.pathRegex === "string") {
|
||||
try {
|
||||
const regex = new RegExp(rule.pathRegex);
|
||||
if (!regex.test(ctx.url)) return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
if (!pathPatternMatches(rule.pathRegex, ctx.url)) return false;
|
||||
}
|
||||
if (typeof rule.contains === "string" && !ctx.url.includes(rule.contains)) {
|
||||
return false;
|
||||
@@ -137,6 +132,16 @@ function ruleMatches(rule, ctx) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function pathPatternMatches(pattern, url) {
|
||||
if (pattern === "^/auth/me(?:\\?.*)?$") {
|
||||
return url === "/auth/me" || url.startsWith("/auth/me?");
|
||||
}
|
||||
if (/^[a-zA-Z0-9_./?=&:-]+$/.test(pattern)) {
|
||||
return url === pattern;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function maybeApplyGlobalBehavior(ctx) {
|
||||
const mockBehavior = behavior();
|
||||
const baseDelay = Number(mockBehavior.globalDelayMs || 0);
|
||||
|
||||
Reference in New Issue
Block a user