fix(codeql): harden release alert sinks (#4554)

This commit is contained in:
Steven Enamakel
2026-07-05 01:05:17 -07:00
committed by GitHub
parent b20c287b9f
commit 392d07662f
6 changed files with 88 additions and 41 deletions
+10 -3
View File
@@ -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
+12 -4
View File
@@ -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);
+8 -3
View File
@@ -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', {});
+10 -8
View File
@@ -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) {
+37 -17
View File
@@ -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) => ({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;",
})[c]);
return String(s).replace(
/[&<>"']/g,
(c) =>
({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;",
})[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>
+11 -6
View File
@@ -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);