CVE-2024-5549 (Python, published 2024-07-09T00:15Z)
Description: value: A CORS misconfiguration in the stitionai/devika repository allows attackers to steal sensitive information such as logs, browser sessions, and settings containing private API keys from other services. This vulnerability also enables attackers to perform actions on behalf of the user, such as deleting projects or sending messages. The issue arises from the lack of proper origin validation, allowing unauthorized cross-origin requests to be executed. The vulnerability is present in all versions of the repository, as no fixed version has been specified.
Severity: nan (CVSS v3 base score: )
Weakness: CWE-346 \u2014 Origin Validation Error
The product does not properly verify that the source of data or communication is valid.
Repository: https://github.com/stitionai/devika
Files changed:
devika.py
src/apis/project.py
src/project.py
src/state.py
ui/package.json
ui/src/lib/components/MessageInput.svelte
Commit message: Fix: Sanitizing HTML inputs and Cors misconfiguration


Vulnerable code:
CORS(app)
@app.route("/api/get-project-files/", methods=["GET"])
@route_logger(logger)
def project_files():
    project_name = request.args.get("project_name")
    files = AgentState.get_project_files(project_name)  
    return jsonify({"files": files})


    manager.create_project(project_name)
    project_name = data.get("project_name")
    project_name = request.args.get("project_name")
    project_name = request.args.get("project_name")

    def get_project_files(self, project_name: str):
        if not project_name:
            return []
        project_directory = "-".join(project_name.split(" "))
        directory = os.path.join(os.getcwd(), 'data', 'projects', project_directory) 
        if(not os.path.exists(directory)):
            return []
        files = []
        for root, _, filenames in os.walk(directory):
            for filename in filenames:
                file_relative_path = os.path.relpath(root, directory)
                if file_relative_path == '.': file_relative_path = ''
                file_path = os.path.join(file_relative_path, filename)
                print("file_path",file_path)
                try:
                    with open(os.path.join(root, filename), 'r') as file:
                        print("File:", filename)
                        files.append({
                            "file": file_path,
                            "code": file.read()
                        })
                except Exception as e:
                    print(f"Error reading file {filename}: {e}")
        return files
    if (value == null){
    if (messageInput.trim() !== "" && isSending) {
      emitMessage("user-message", { 
        message: messageInput,
      
      if(data['type'] == 'time') {
       
      <div class="px-1 rounded-md text-xs">
        Model Inference: <span class="text-orange-600">{inference_time} sec</span>
      </div>
<div class="expandable-input relative">
  <textarea
    id="message-input"
    class="w-full p-4 font-medium focus:text-foreground rounded-xl outline-none h-28 pr-20 bg-secondary
    {$isSending ? 'cursor-not-allowed' : ''}"   
    placeholder="Type your message..."
    disabled={$isSending}
    bind:value={messageInput}
    on:input={setTokenSize}
    on:keydown={(e) => {
      if (e.key === "Enter" && !e.shiftKey) {
        e.preventDefault();
        handleSendMessage();
        document.querySelector('.token-count').textContent = 0;
      }
    }}
  ></textarea>
  <button 
    on:click={handleSendMessage}
    disabled={$isSending}
    class="absolute text-secondary bg-primary p-2 right-4 bottom-6 rounded-full
  >
  {@html Icons.CornerDownLeft} 
  </button>
  <p class="absolute text-tertiary p-2 right-4 top-2">
    <span class="token-count">0</span>
  </p>
</div>

Fixed code:
CORS(app, resources={r"/*": {"origins": ["https://localhost:3000"]}}) # Change the origin to your frontend URL
from werkzeug.utils import secure_filename

@project_bp.route("/api/get-project-files", methods=["GET"])
@route_logger(logger)
def project_files():
    project_name = secure_filename(request.args.get("project_name"))
    files = manager.get_project_files(project_name)  
    return jsonify({"files": files})

    manager.create_project(secure_filename(project_name))
    project_name = secure_filename(data.get("project_name"))
    project_name = secure_filename(request.args.get("project_name"))
    project_name = secure_filename(request.args.get("project_name"))
    
    def get_project_files(self, project_name: str):
        if not project_name:
            return []

        project_directory = "-".join(project_name.split(" "))
        base_path = os.path.abspath(os.path.join(os.getcwd(), 'data', 'projects'))
        directory = os.path.join(base_path, project_directory)

        # Ensure the directory is within the allowed base path
        if not os.path.exists(directory) or not os.path.commonprefix([directory, base_path]) == base_path:
            return []

        files = []
        for root, _, filenames in os.walk(directory):
            for filename in filenames:
                file_relative_path = os.path.relpath(root, directory)
                if file_relative_path == '.':
                    file_relative_path = ''
                file_path = os.path.join(file_relative_path, filename)
                try:
                    with open(os.path.join(root, filename), 'r') as file:
                        files.append({
                            "file": file_path,
                            "code": file.read()
                        })
                except Exception as e:
                    print(f"Error reading file {filename}: {e}")
        return files
        
    "dompurify": "^3.1.5",
  import DOMPurify from "dompurify";
    if (value == null) {

  // Function to escape HTML
  function escapeHTML(input) {
    const map = {
      "&": "&amp;",
      "<": "&lt;",
      ">": "&gt;",
      '"': "&quot;",
      "'": "&#039;",
    };
    return input.replace(/[&<>"']/g, function (m) {
      return map[m];
    });
  }

    const sanitizedMessage = DOMPurify.sanitize(messageInput);
    const escapedMessage = escapeHTML(sanitizedMessage);


    if (messageInput.trim() !== "" && escapedMessage.trim() !== "" && isSending) {
      emitMessage("user-message", {
        message: escapedMessage,
      if (data["type"] == "time") {

    <div class="px-1 rounded-md text-xs">
      Model Inference: <span class="text-orange-600">{inference_time} sec</span>
    </div>
  <div class="expandable-input relative">
    <textarea
      id="message-input"
      class="w-full p-4 font-medium focus:text-foreground rounded-xl outline-none h-28 pr-20 bg-secondary
      placeholder="Type your message..."
      disabled={$isSending}
      bind:value={messageInput}
      on:input={setTokenSize}
      on:keydown={(e) => {
        if (e.key === "Enter" && !e.shiftKey) {
          e.preventDefault();
          handleSendMessage();
          document.querySelector(".token-count").textContent = 0;
        }
      }}
    ></textarea>
    <button
      on:click={handleSendMessage}
      disabled={$isSending}
      class="absolute text-secondary bg-primary p-2 right-4 bottom-6 rounded-full
    {$isSending ? 'cursor-not-allowed' : ''}"
    >
      {@html Icons.CornerDownLeft}
    </button>
    <p class="absolute text-tertiary p-2 right-4 top-2">
      <span class="token-count">0</span>
    </p>
  </div>