CVE-2024-5547 (Python, published 2024-06-27T18:15Z)
Description: value: A directory traversal vulnerability exists in the /api/download-project-pdf endpoint of the stitionai/devika repository, affecting the latest version. The vulnerability arises due to insufficient sanitization of the 'project_name' parameter in the download_project_pdf function. Attackers can exploit this flaw by manipulating the 'project_name' parameter in a GET request to traverse the directory structure and download arbitrary PDF files from the system. This issue allows attackers to access sensitive information that could be stored in PDF format outside the intended directory.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
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>