Skip to content

Conversation

@KesterTan
Copy link
Contributor

@KesterTan KesterTan commented Oct 6, 2025

Description

Previously, courses, labs and assessments that were downloaded as tar files ended up being corrupted. There was also no quick and easy way to view permission bits and change permissions of files on the system.

This PR allows

  • Courses and labs to be downloaded and exported correctly as tar files
  • Tar files can be exported and opened in the file manager without affecting the core project directory
  • File manager shows permission bits of each file and directory
Screenshot 2025-10-06 at 4 06 27 PM

How Has This Been Tested?

  1. Create new course and assessments
  2. Check that they appear in the file manager
  3. Click download tar and check that you can un-tar the file after it is downloaded
  4. Upload the tar file and check that you can extract the contents of the tar file
  5. Check that you can view permission bits of a file, try changing one of the permission bits and check that the permission bits are as you expect

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • I have run rubocop and erblint for style check. If you haven't, run overcommit --install && overcommit --sign to use pre-commit hook for linting
  • My change requires a change to the documentation, which is located at Autolab Docs
  • I have updated the documentation accordingly, included in this PR

If unsure, feel free to submit first and we'll help you along.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 6, 2025

📝 Walkthrough

Walkthrough

Adds tar archive support (preview, list contents, view single entry, stream/generate tars) and permissions display in listings; introduces multiple controller helpers for path mapping and tar handling, a new tar viewing/preview UI, a permissions column, a standalone import_submissions.rb script, and minor JS/CSS formatting and a spec update.

Changes

Cohort / File(s) Summary
Controller: tar + permissions + helpers
app/controllers/file_manager_controller.rb
Adds many methods: tar detection (is_tar_file?), listing (list_tar_contents), single-file extraction (extract_single_file_from_tar), tar streaming/generation (download_tar, add_directory_to_tar, tar_writer flow), tar viewing (view_tar_file), directory/path helpers (find_by_directory_path, find_by_folder_path, path_belongs_to_assessment?, path_belongs_to_course?), integrates permissions into directory listings, and robust error handling / normalization for tar operations.
Views: UI, tar preview & file view
app/views/file_manager/index.html.erb, app/views/file_manager/tar_preview.html.erb, app/views/file_manager/tar_file_view.html.erb
Adds Permissions column to listing; special rendering and actions for tar archives (preview, download, view entry); new templates for archive preview and rendering a single extracted file with syntax highlighting and controls.
Routes
config/routes.rb
Adds route: get 'file_manager/view_tar_file'file_manager#view_tar_file (named view_tar_file).
Frontend JS (formatting)
app/assets/javascripts/file_manager.js
Formatting/whitespace changes only (no behavior change) per diff summary; no new JS behavior introduced in this PR.
Frontend styles
app/assets/stylesheets/file_manager.scss
Reindent/formatting changes and adds .permissions-display style (monospace, small font, dark color).
Spec
spec/controllers/file_manager_controller_spec.rb
Adds expectation asserting a "Permissions" table header in index HTML tests.
Utility script (new)
import_submissions.rb
New standalone script to bulk-import submissions from a ZIP: extracts per-user tar handins, finds users/CUDs, creates Submission records and uploads files; includes per-entry error handling and cleanup.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant UI as FileManager UI
  participant FM as FileManagerController
  participant FS as Filesystem
  Note over UI,FM: Permissions display and tar actions available in listing

  User->>UI: Click "Preview Contents" (tar)
  UI->>FM: GET /file_manager?path=...&preview=true
  FM->>FM: is_tar_file? -> list_tar_contents(path)
  FM->>FM: build preview model (entries with permissions)
  FM-->>UI: Render tar_preview (list of entries)
Loading
sequenceDiagram
  autonumber
  actor User
  participant UI as FileManager UI
  participant FM as FileManagerController
  participant Tar as Tar/Gzip libs
  participant FS as Filesystem

  Note over UI,FM: View single file inside tar
  User->>UI: Click entry link (view_tar_file)
  UI->>FM: GET file_manager/view_tar_file?tar_path=...&file_path=...
  FM->>FM: authorize + validate tar format
  FM->>Tar: extract_single_file_from_tar(tar_path, file_path)
  Tar-->>FM: file payload or error
  alt success (text)
    FM-->>UI: render tar_file_view with file contents
  else success (binary/attachment)
    FM-->>UI: send_data as attachment
  else error
    FM-->>UI: redirect with flash error
  end
Loading
sequenceDiagram
  autonumber
  actor User
  participant UI as FileManager UI
  participant FM as FileManagerController
  participant Tar as Tar writer
  participant FS as Filesystem

  User->>UI: Click "Download" on directory/course/assessment
  UI->>FM: GET download (target)
  alt single file
    FM->>FS: read file
    FM-->>UI: send file
  else directory / assessment / course
    FM->>Tar: stream tar via add_directory_to_tar / tar_writer
    FM->>FS: read entries recursively
    Tar-->>FM: tar stream
    FM-->>UI: stream tar download
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Files/areas needing extra attention:

  • app/controllers/file_manager_controller.rb — large surface area: tar streaming, extraction, authorization, path normalization, and recursive tar generation.
  • Tar safety checks: traversal normalization, symlink handling, and extraction write paths.
  • New views tar_preview.html.erb and tar_file_view.html.erb — ensure correct escaping and safe rendering of file contents.
  • import_submissions.rb — uses Rack test upload flow and filesystem temp handling; verify security assumptions and hardcoded course name.

Possibly related PRs

  • File manager exception fix #2229 — Modifies app/controllers/file_manager_controller.rb and overlaps with download/tar and authorization logic; likely intersecting changes.

Suggested reviewers

  • jlge
  • evanyeyeye

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'File manager fixes' is vague and generic, using non-descriptive language that doesn't convey meaningful information about the specific changes. Revise the title to be more specific, such as 'Add tar file support and permissions display to file manager' or 'Enable tar file previews and permissions management in file manager'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The pull request description covers all major required sections: clear description of changes, motivation (fixing corruption), comprehensive testing steps, and proper checklist completion with style checks marked as done.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch file-manager-fixes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (13)
import_submissions.rb (3)

18-18: Hardcoded course name requires manual editing.

The hardcoded course name "18213-m25" on line 18 requires developers to manually edit the script for each use. This reduces maintainability and increases the risk of errors.

Accept the course name as a CLI argument:

-zip_path = ARGV[0]
-assessment_name = ARGV[1]
+zip_path = ARGV[0]
+course_name = ARGV[1]
+assessment_name = ARGV[2]

 unless zip_path && assessment_name
-  puts "Usage: rails runner import_submissions.rb path/to/zipfile.zip \"Assessment Name\""
+  puts "Usage: rails runner import_submissions.rb path/to/zipfile.zip \"Course Name\" \"Assessment Name\""
   exit 1
 end

-# Replace this with your actual course ID
-course_name = "18213-m25"  # <-- change this to your course name
 course = Course.find_by(name: course_name)

37-37: Regex could be more precise.

The regex pattern on line 37 uses \d+? (non-greedy) for the ID field, but the ? quantifier is unnecessary here since \d+ will match the digits before the underscore delimiter naturally due to the fixed pattern structure.

Simplify to:

-next unless entry.name =~ /\A(.+?)_\d+?_#{Regexp.escape(assessment_name)}-handin\.tar\z/
+next unless entry.name =~ /\A(.+?)_\d+_#{Regexp.escape(assessment_name)}-handin\.tar\z/

66-73: Error handling doesn't distinguish error types.

The generic rescue => e on line 71 catches all exceptions, making it difficult to diagnose specific failure modes (database errors, file system errors, validation errors, etc.).

Add more specific rescue clauses:

 begin
   submission.save!
   file_param = { "file" => Rack::Test::UploadedFile.new(temp_tar_path, "application/x-tar") }
   submission.save_file(file_param)
   puts "Successfully created submission for #{username}"
-rescue => e
-  puts "Failed to create submission for #{username}: #{e.message}"
+rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e
+  puts "Failed to save submission for #{username}: #{e.message}"
+rescue Errno::ENOENT, Errno::EACCES => e
+  puts "File system error for #{username}: #{e.message}"
+rescue StandardError => e
+  puts "Unexpected error for #{username}: #{e.class} - #{e.message}"
 end
app/assets/stylesheets/file_manager.scss (1)

137-142: Commented-out justify-content suggests uncertainty.

The commented-out justify-content: center; on line 141 indicates potential indecision about the layout alignment.

Either enable the centering or remove the commented line:

 .permissions-container {
   display: flex;
   align-items: center;
   gap: 8px;
-  // justify-content: center;
 }
app/views/file_manager/index.html.erb (2)

64-75: Extract confirmation message could be more informative.

The confirmation dialog on line 73 says "This will extract the tar file to a new subdirectory" but doesn't specify which subdirectory will be created, making it harder for users to predict the outcome.

Include the target directory name in the confirmation:

In the controller, pass the extraction directory name to the view, or show it in JavaScript:

<%= link_to "Extract", "#{entry[:relative]}?extract=true",
            onclick: "return confirm('This will extract the contents to: #{File.basename(entry[:entry], '.*').gsub(/\.tar$/, '')}/ - Continue?');",
            style: "margin-right: 10px; font-size: 12px; color: #4CAF50;" %>

94-95: Maxlength and pattern mismatch.

The maxlength="4" on line 94 allows 4 characters, but the pattern [0-7]{3,4} also accepts 3-digit permissions. A user entering 755 would see it as valid (per pattern) even though they could add a 4th digit.

Clarify if 3 or 4 digits are expected. For typical Unix permissions, 3 digits suffice:

-maxlength="4"
-pattern="[0-7]{3,4}"
-title="Enter 3-4 digit octal permissions (e.g., 755, 644)">
+maxlength="3"
+pattern="[0-7]{3}"
+title="Enter 3 digit octal permissions (e.g., 755, 644)">

If 4-digit permissions (with setuid/setgid/sticky bits) are needed, keep maxlength="4" but update the pattern to require exactly 4 when a 4th digit is present:

pattern="[0-7]{3,4}"

is already correct in that case, but ensure backend validation also accepts 4-digit input.

app/assets/javascripts/file_manager.js (1)

244-256: Real-time validation overrides CSS classes.

The input event handler on lines 244-256 directly sets style.borderColor and style.backgroundColor, which will override any CSS classes and make the styling harder to maintain.

Use CSS classes instead of inline styles:

-input.addEventListener('input', function(e) {
-    const value = this.value;
-    if (value && !/^[0-7]{0,4}$/.test(value)) {
-        this.style.borderColor = '#dc3545';
-        this.style.backgroundColor = '#ffe6e6';
-    } else if (value && /^[0-7]{3,4}$/.test(value)) {
-        this.style.borderColor = '#28a745';
-        this.style.backgroundColor = '#e6ffe6';
-    } else {
-        this.style.borderColor = '#ddd';
-        this.style.backgroundColor = '#f9f9f9';
-    }
-});
+input.addEventListener('input', function(e) {
+    const value = this.value;
+    this.classList.remove('permissions-invalid', 'permissions-valid', 'permissions-neutral');
+    if (value && !/^[0-7]{0,4}$/.test(value)) {
+        this.classList.add('permissions-invalid');
+    } else if (value && /^[0-7]{3,4}$/.test(value)) {
+        this.classList.add('permissions-valid');
+    } else {
+        this.classList.add('permissions-neutral');
+    }
+});

Then define these classes in file_manager.scss:

input[type=text].permissions-input {
  // ... existing styles ...
  
  &.permissions-invalid {
    border-color: #dc3545;
    background-color: #ffe6e6;
  }
  
  &.permissions-valid {
    border-color: #28a745;
    background-color: #e6ffe6;
  }
  
  &.permissions-neutral {
    border-color: #ddd;
    background-color: #f9f9f9;
  }
}
app/controllers/file_manager_controller.rb (6)

61-61: Directory creation could fail silently.

FileUtils.mkdir_p on line 61 can fail due to permission issues, but there's no explicit error handling until the rescue blocks later. If directory creation fails, subsequent operations will fail with less clear error messages.

Add explicit check:

 # Create the extraction directory
-FileUtils.mkdir_p(extract_dir)
+begin
+  FileUtils.mkdir_p(extract_dir)
+rescue Errno::EACCES, Errno::ENOSPC => e
+  flash[:error] = "Cannot create extraction directory: #{e.message}"
+  redirect_to file_manager_index_path(path: File.dirname(path))
+  return
+end

88-107: Rescue blocks should avoid returning nil.

Lines 87, 94, 100, and 106 explicitly return nil after setting flash and redirecting. This is unnecessary and can be confusing since Rails automatically handles the response after redirect.

Remove the explicit nil returns:

 rescue Gem::Package::TarInvalidError => e
   # Clean up partially extracted directory
   FileUtils.rm_rf(extract_dir) if File.exist?(extract_dir)
   flash[:error] =
     "Corrupted tar file: #{e.message}. The file appears to be damaged or invalid."
   redirect_to file_manager_index_path(path: File.dirname(path))
-  nil
 rescue Zlib::GzipFile::Error => e
   # ...
-  nil
 rescue StandardError => e
   # ...
-  nil
 end

177-226: Chmod action has good validation but could improve error responses.

The chmod action properly validates octal format and permission ranges, but the error responses (lines 207-225) use both flash messages and JSON responses. This dual approach is good for supporting different client types.

However, consider adding a success JSON response for consistency:

 # Apply the permission change
 File.chmod(permission_mode, absolute_path)
-flash[:success] = "Successfully changed permissions to #{params[:permissions]}"
+flash[:success] = "Successfully changed permissions to #{params[:permissions]}"
+respond_to do |format|
+  format.html { redirect_back(fallback_location: file_manager_index_path) }
+  format.json { render json: { success: true, permissions: params[:permissions] }, status: :ok }
+end

482-483: Path traversal checks are good but could be more explicit.

Lines 482-483 check for .. and absolute paths in entry names, which prevents basic path traversal. However, consider using a more robust normalization approach.

Add explicit path normalization:

 tar_reader.each do |entry|
-  # Skip entries with invalid names or paths that try to escape the extraction directory
-  next if entry.full_name.include?('..')
-  next if entry.full_name.start_with?('/')
+  # Skip entries with invalid names or paths that try to escape the extraction directory
+  next if entry.full_name.include?('..')
+  next if entry.full_name.start_with?('/')
+  
+  # Additional normalization to catch other edge cases
+  normalized_name = Pathname.new(entry.full_name).cleanpath.to_s
+  next if normalized_name.start_with?('../') || normalized_name.start_with?('/')

   # Handle entries that might be at the root level or in subdirectories
   target_path = File.join(extract_dir, entry.full_name)
+  
+  # Verify the target path is within extract_dir after joining
+  unless target_path.start_with?(extract_dir)
+    Rails.logger.warn "Skipping entry #{entry.full_name}: path traversal detected"
+    next
+  end

501-527: Chunked reading with fallback is good, but logging could improve.

The chunked reading implementation (lines 501-527) handles corruption well with a fallback to full read. The warning log at line 515 is helpful.

Consider adding more context to the log:

-Rails.logger.warn "Chunked reading failed for #{entry.full_name}:
-  #{e.message}, trying full read"
+Rails.logger.warn "Chunked reading failed for #{entry.full_name} (expected size: #{expected_size}): #{e.message}, trying full read"

576-633: is_likely_executable? is comprehensive but could be simpler.

The method thoroughly checks for executables using extensions, names, content signatures, and file headers (ELF, Mach-O, PE, etc.). However, it's complex and may over-detect executables.

Consider if this level of detection is necessary. For most course materials, checking the original tar entry's executable bit (which is already done in extract_tar_entries) may be sufficient. If you need this level of detection, the current implementation is good.

If simplification is desired:

def is_likely_executable?(file_path, original_name)
  # Simplified: check common script extensions and shebang
  executable_extensions = ['.sh', '.bash', '.zsh', '.py', '.pl', '.rb']
  extension = File.extname(original_name).downcase
  return true if executable_extensions.include?(extension)
  
  # Check for shebang
  if File.exist?(file_path) && File.readable?(file_path) && File.size(file_path) > 0
    first_bytes = File.read(file_path, 2)
    return true if first_bytes == "#!"
  end
  
  false
end
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4ded082 and ba9e925.

📒 Files selected for processing (7)
  • app/assets/javascripts/file_manager.js (9 hunks)
  • app/assets/stylesheets/file_manager.scss (1 hunks)
  • app/controllers/file_manager_controller.rb (4 hunks)
  • app/views/file_manager/index.html.erb (2 hunks)
  • config/routes.rb (1 hunks)
  • import_submissions.rb (1 hunks)
  • spec/controllers/file_manager_controller_spec.rb (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
app/controllers/file_manager_controller.rb (2)
app/models/assessment.rb (3)
  • path (505-507)
  • folder_path (93-95)
  • load_dir_to_tar (467-490)
app/models/course.rb (2)
  • directory_path (52-54)
  • generate_tar (340-378)
import_submissions.rb (1)
app/models/submission.rb (1)
  • save_file (82-116)
🪛 ast-grep (0.39.5)
app/controllers/file_manager_controller.rb

[warning] 301-301: Found the use of an hardcoded passphrase for RSA. The passphrase can be easily discovered, and therefore should not be stored in source-code. It is recommended to remove the passphrase from source-code, and use system environment variables or a restricted configuration file.
Context: assessment.load_dir_to_tar(absolute_path.to_s, "", tar_assessment, filter, "")
Note: [CWE-798]: Use of Hard-coded Credentials [OWASP A07:2021]: Identification and Authentication Failures [REFERENCES]
https://cwe.mitre.org/data/definitions/522.html

(hardcoded-secret-rsa-passphrase-ruby)

🪛 Brakeman (7.1.0)
app/controllers/file_manager_controller.rb

[weak] 61-61: Parameter value used in file name
Type: File Access
Confidence: Weak
More info: https://brakemanscanner.org/docs/warning_types/file_access/

(File Access)


[weak] 77-77: Parameter value used in file name
Type: File Access
Confidence: Weak
More info: https://brakemanscanner.org/docs/warning_types/file_access/

(File Access)


[weak] 103-103: Parameter value used in file name
Type: File Access
Confidence: Weak
More info: https://brakemanscanner.org/docs/warning_types/file_access/

(File Access)


[weak] 90-90: Parameter value used in file name
Type: File Access
Confidence: Weak
More info: https://brakemanscanner.org/docs/warning_types/file_access/

(File Access)


[weak] 97-97: Parameter value used in file name
Type: File Access
Confidence: Weak
More info: https://brakemanscanner.org/docs/warning_types/file_access/

(File Access)


[weak] 312-312: Parameter value used in file name
Type: File Access
Confidence: Weak
More info: https://brakemanscanner.org/docs/warning_types/file_access/

(File Access)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test
🔇 Additional comments (7)
spec/controllers/file_manager_controller_spec.rb (1)

16-16: LGTM! Test coverage for new Permissions column.

The added expectation correctly validates the presence of the "Permissions" table header in the file manager index view, aligning with the new permissions management feature introduced in this PR.

config/routes.rb (1)

84-84: LGTM! Route definition is correct.

The new chmod route is properly defined with appropriate HTTP method (PATCH), path constraint, and routing target. The pattern aligns with existing file_manager routes and enables the new permissions management feature.

app/assets/stylesheets/file_manager.scss (2)

93-111: LGTM! Input styling with good UX.

The permissions input styling provides clear visual feedback through focus, invalid, and valid states. The monospace font is appropriate for octal permissions display.


113-135: LGTM! Button styling is accessible.

The permissions button has appropriate sizing, color states, and uses flexbox for icon alignment. Hover and active states provide good user feedback.

app/controllers/file_manager_controller.rb (3)

478-574: Extract_tar_entries has proper safety checks.

The extract_tar_entries method includes good security measures:

  • Path traversal prevention (line 482)
  • Absolute path check (line 483)
  • Size validation during extraction (lines 509-511)
  • Fallback reading for corrupted entries (lines 517-526)
  • Permission handling with fallbacks (lines 537-564)

636-676: Tar validation is thorough and secure.

The validate_tar_file method properly validates tar files before extraction, checking readability, size, and format integrity for both gzipped and regular tar files. This prevents malformed tar files from causing issues during extraction.


415-419: Permissions field addition in populate_directory.

The addition of the permissions field (lines 415-419) correctly formats Unix permissions as octal strings and includes error handling for cases where permissions can't be retrieved.

return;
}

let rel_path = decodeURIComponent(path.split("/file_manager/")[1]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Path manipulation could fail on edge cases.

Line 213 splits the path by "/file_manager/" and assumes the relevant path is after the first occurrence. This could break if the path itself contains the string "/file_manager/".

Use a more robust approach. If path is already the relative path as used elsewhere in the file, you might not need this splitting at all:

-let rel_path = decodeURIComponent(path.split("/file_manager/")[1]);
+// Assuming path is already the relative path from entry[:relative]
+let rel_path = decodeURIComponent(path);

Alternatively, if you must extract from a full URL:

// Remove the /file_manager/ prefix if present
let rel_path = path.replace(/^\/file_manager\//, '');
rel_path = decodeURIComponent(rel_path);
🤖 Prompt for AI Agents
In app/assets/javascripts/file_manager.js around line 213, the current code
splits the path on "/file_manager/" and takes the second segment which fails if
the path contains that string multiple times; instead remove only a leading
"/file_manager/" prefix if present (or skip any splitting entirely if path is
already relative), then decodeURIComponent the resulting string; implement this
by checking and stripping the prefix at the start of the string and then calling
decodeURIComponent on the result.

Comment on lines 36 to 42
if params[:extract] && is_tar_file?(absolute_path)
# Skip extraction if the tar file itself is an executable
if is_likely_executable?(absolute_path, File.basename(absolute_path))
flash[:error] = "Cannot extract tar files that are not directories"
redirect_to file_manager_index_path(path: File.dirname(path))
return
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify tar extraction is properly authorized.

The tar extraction feature checks check_instructor(absolute_path) but proceeds to extract to a sibling directory in the parent. Ensure the authorization check covers write permissions to the parent directory, not just read access to the tar file.

Run this script to verify how check_instructor determines authorization and whether it validates write access:


🏁 Script executed:

#!/bin/bash
# Check the implementation of check_instructor and related authorization logic
ast-grep --pattern $'def check_instructor($PATH)
  $$$
end'

# Also check if there are any other authorization checks for file operations
rg -A 10 'def check_instructor'

Length of output: 1846


Validate write authorization on extraction directory

check_instructor currently only confirms access to the tar file’s path, but does not authorize write operations in the parent/sibling directory where files are extracted. Add a check (e.g. check_instructor(destination_path)) or explicitly verify the user has write permission on the target directory before extraction.

🤖 Prompt for AI Agents
In app/controllers/file_manager_controller.rb around lines 36 to 42, the code
validates access to the tar file but does not verify that the user is authorized
to write into the extraction destination; add an explicit authorization check
for the target directory before performing extraction (for example call
check_instructor on the destination_path or otherwise verify the current user
has write permission on File.dirname(absolute_path) or the intended extraction
directory), and if the check fails set an error flash and redirect back without
extracting.

Comment on lines +228 to +253
def find_by_directory_path(absolute_path)
# Normalize the path to ensure consistent comparison
normalized_path = Pathname.new(absolute_path).cleanpath.to_s
# Find course where the directory_path matches the given absolute path
Course.find do |course|
course.directory_path.to_s == normalized_path
end
end

def find_by_folder_path(absolute_path)
# Normalize the path to ensure consistent comparison
normalized_path = Pathname.new(absolute_path).cleanpath.to_s

# Find assessment where the folder_path matches the given absolute path
Assessment.find do |assessment|
assessment.folder_path.to_s == normalized_path
end
end

def path_belongs_to_assessment?(absolute_path)
find_by_folder_path(absolute_path)
end

def path_belongs_to_course?(absolute_path)
find_by_directory_path(absolute_path)
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Path lookup methods use inefficient iteration.

Methods find_by_directory_path and find_by_folder_path (lines 228-245) use Course.find and Assessment.find which iterate through all records. This is inefficient for large datasets.

Use database queries instead:

-def find_by_directory_path(absolute_path)
-  # Normalize the path to ensure consistent comparison
-  normalized_path = Pathname.new(absolute_path).cleanpath.to_s
-  # Find course where the directory_path matches the given absolute path
-  Course.find do |course|
-    course.directory_path.to_s == normalized_path
-  end
-end
+def find_by_directory_path(absolute_path)
+  normalized_path = Pathname.new(absolute_path).cleanpath.to_s
+  # Extract course name from path (assuming structure: BASE_DIRECTORY/course_name/...)
+  course_name = normalized_path.sub(BASE_DIRECTORY.to_s + '/', '').split('/').first
+  Course.find_by(name: course_name) if course_name
+end

Similarly for find_by_folder_path:

-def find_by_folder_path(absolute_path)
-  # Normalize the path to ensure consistent comparison
-  normalized_path = Pathname.new(absolute_path).cleanpath.to_s
-
-  # Find assessment where the folder_path matches the given absolute path
-  Assessment.find do |assessment|
-    assessment.folder_path.to_s == normalized_path
-  end
-end
+def find_by_folder_path(absolute_path)
+  normalized_path = Pathname.new(absolute_path).cleanpath.to_s
+  # Extract course and assessment names from path
+  path_parts = normalized_path.sub(BASE_DIRECTORY.to_s + '/', '').split('/')
+  if path_parts.length >= 2
+    course = Course.find_by(name: path_parts[0])
+    course&.assessments&.find_by(name: path_parts[1])
+  end
+end

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/controllers/file_manager_controller.rb around lines 228-253, the current
methods iterate all records using Course.find and Assessment.find which is
inefficient; replace them with ActiveRecord queries that use the normalized_path
in the WHERE clause (e.g. Course.find_by(directory_path: normalized_path) and
Assessment.find_by(folder_path: normalized_path)) to let the DB do the lookup,
and update path_belongs_to_assessment? and path_belongs_to_course? to return a
boolean (e.g. use .present? on the result) if intended.

Comment on lines +287 to +309
course = path_belongs_to_course?(absolute_path)
assessment = path_belongs_to_assessment?(absolute_path)

if course.present?
tar_course = generate_tar(course)
send_data tar_course.string.force_encoding("binary"),
filename: "#{course.name}.tar",
type: "application/x-tar",
disposition: "attachment"
elsif assessment.present?
tar_stream = StringIO.new("")
Gem::Package::TarWriter.new(tar_stream) do |tar|
Dir[File.join(absolute_path.to_s, '**', '**')].each do |file|
mode = File.stat(file).mode
relative_path = file.sub(%r{^#{Regexp.escape(absolute_path.to_s)}/?}, '')
if File.directory?(file)
tar.mkdir relative_path, mode
else
tar.add_file relative_path, mode do |tar_file|
File.open(file, "rb") { |f| tar_file.write f.read }
end
end
end
Gem::Package::TarWriter.new(tar_stream) do |tar_assessment|
assessment.name
assessment.dump_yaml
filter = []
assessment.load_dir_to_tar(absolute_path.to_s, "", tar_assessment, filter, "")
end
tar_stream.rewind
tar_stream.close
send_data tar_stream.string.force_encoding("binary"),
filename: "file_manager.tar",
filename: "#{assessment.name}.tar",
type: "application/x-tar",
disposition: "attachment"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Tar generation logic duplicates course export.

The download_tar method's handling of course tar generation (lines 290-295) appears to duplicate logic from the Course model's generate_tar method. However, line 291 calls generate_tar(course) passing the course object, but the Course#generate_tar method (from relevant code snippets) expects export_configs, not a course.

Fix the method call or clarify the parameter:

 if course.present?
-  tar_course = generate_tar(course)
+  # The generate_tar method should be called on the course instance
+  tar_course = course.generate_tar(['assessments', 'metrics_config'])
   send_data tar_course.string.force_encoding("binary"),
             filename: "#{course.name}.tar",
             type: "application/x-tar",
             disposition: "attachment"
🧰 Tools
🪛 ast-grep (0.39.5)

[warning] 301-301: Found the use of an hardcoded passphrase for RSA. The passphrase can be easily discovered, and therefore should not be stored in source-code. It is recommended to remove the passphrase from source-code, and use system environment variables or a restricted configuration file.
Context: assessment.load_dir_to_tar(absolute_path.to_s, "", tar_assessment, filter, "")
Note: [CWE-798]: Use of Hard-coded Credentials [OWASP A07:2021]: Identification and Authentication Failures [REFERENCES]
https://cwe.mitre.org/data/definitions/522.html

(hardcoded-secret-rsa-passphrase-ruby)

🤖 Prompt for AI Agents
In app/controllers/file_manager_controller.rb around lines 287 to 309, the
controller calls generate_tar(course) which duplicates course export logic and
passes a Course object to a method that expects export_configs; change the call
to use the Course instance method or supply the expected export_configs: either
call course.generate_tar(...) if generate_tar is an instance method (passing any
required export_configs), or call the module function with course.export_configs
(or appropriate config array) so the correct parameter type is provided and the
duplicate tar-building logic is removed.

Comment on lines +58 to +63
<%
# Check if file is a tar archive
file_ext = File.extname(entry[:entry]).downcase
is_tar = file_ext == '.tar' || file_ext == '.tgz' ||
(file_ext == '.gz' && entry[:entry].downcase.end_with?('.tar.gz'))
%>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Tar detection duplicates controller logic.

The tar detection logic on lines 58-63 is duplicated between the view and controller (controller's is_tar_file? method). This violates DRY and can lead to inconsistencies.

Move tar detection to a helper method or use the controller's detection result:

In the controller's populate_directory method:

{
  # ... existing fields ...
  is_tar: is_tar_file?(abs_path_str),
  # ...
}

Then in the view:

-<%
-  # Check if file is a tar archive
-  file_ext = File.extname(entry[:entry]).downcase
-  is_tar = file_ext == '.tar' || file_ext == '.tgz' ||
-           (file_ext == '.gz' && entry[:entry].downcase.end_with?('.tar.gz'))
-%>
-<% if entry[:type] == :file && is_tar %>
+<% if entry[:type] == :file && entry[:is_tar] %>
🤖 Prompt for AI Agents
In app/views/file_manager/index.html.erb around lines 58-63, the tar-detection
logic is duplicated; remove the inline File.extname-based detection and use a
single source of truth. Update the controller method that builds directory
entries (populate_directory) to set an is_tar boolean on each entry (e.g., using
the existing is_tar_file? helper), and then in the view replace the removed
logic by reading entry[:is_tar] (or entry["is_tar"] depending on structure).
Ensure the controller populates that key for every entry so the view no longer
computes tar detection itself.

Comment on lines +1 to +5
require 'zip'
require 'fileutils'
require 'tempfile'
require 'rack/test'
include Rack::Test::Methods
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Include statement at module scope can cause side effects.

The include Rack::Test::Methods at line 5 pollutes the global namespace, making all of Rack::Test's methods available in the top-level scope. This can lead to naming conflicts and is not a Rails best practice for scripts.

Move the include inside a module or class to limit its scope:

-require 'zip'
-require 'fileutils'
-require 'tempfile'
-require 'rack/test'
-include Rack::Test::Methods
+require 'zip'
+require 'fileutils'
+require 'tempfile'
+require 'rack/test'
+
+module BulkImport
+  include Rack::Test::Methods
+
+  def self.run
+    # move script logic here
+  end
+end
+
+BulkImport.run

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In import_submissions.rb around lines 1 to 5, the top-level `include
Rack::Test::Methods` pollutes the global namespace; move the `include` into a
narrower scope by placing it inside a module or class that encapsulates the
script's functionality (for example a ImportSubmissions class or a module used
only for test helpers), so only that class/module mixes in Rack::Test::Methods;
keep the `require 'rack/test'` at top but remove the top-level include and add
it as `include Rack::Test::Methods` inside the chosen class/module to limit
scope and avoid global method leakage.


# Extract tar to temporary path
temp_tar_path = File.join(temp_dir, entry.name)
entry.extract(temp_tar_path) { true }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

extract overwrite behavior could cause issues.

The { true } block at line 56 forces overwrite of existing files during extraction. If the temp directory is reused or not properly cleaned up between runs, this could lead to unexpected behavior.

Consider using a unique subdirectory per entry or verifying the extraction path doesn't exist:

 # Extract tar to temporary path
-temp_tar_path = File.join(temp_dir, entry.name)
-entry.extract(temp_tar_path) { true }
+unique_subdir = File.join(temp_dir, SecureRandom.hex(8))
+FileUtils.mkdir_p(unique_subdir)
+temp_tar_path = File.join(unique_subdir, entry.name)
+entry.extract(temp_tar_path)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
entry.extract(temp_tar_path) { true }
# Extract tar to temporary path
- temp_tar_path = File.join(temp_dir, entry.name)
unique_subdir = File.join(temp_dir, SecureRandom.hex(8))
FileUtils.mkdir_p(unique_subdir)
temp_tar_path = File.join(unique_subdir, entry.name)
entry.extract(temp_tar_path)
🤖 Prompt for AI Agents
In import_submissions.rb around line 56, the use of entry.extract(temp_tar_path)
{ true } forces overwriting existing files; change this to avoid unconditional
overwrite by creating a unique extraction path per entry (e.g., use Dir.mktmpdir
or append a UUID/timestamp to temp_tar_path) or check whether the target path
exists before extracting and skip or error if it does; remove the unconditional
block that returns true and instead ensure the extraction target is unique or
validated to prevent accidental replacement of files.

@KesterTan KesterTan requested review from a team and coder6583 and removed request for a team October 21, 2025 03:30
end
end
else
File.open(file_path, 'rb') do |file|

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 4 days ago

To remediate this issue, we should strictly validate user-provided path input before any file access occurs. The best approach is to reject filenames or relative paths containing:

  • Multiple consecutive dots (e.g., .., ...),
  • Directory separators (/ or \),
  • Absolute paths (starting with /),
  • Anything not matching a known whitelist of allowed patterns (e.g., only alphanumeric, underscore, dash, and dot for extensions).

We should implement a method, e.g., sanitize_path, that performs this validation, and invoke it just before safe_expand_path(path) (in check_path_exist). If the path is invalid, we return an error. Place the method in the private section of the controller, and update all code paths where user-provided input (params[:path] or path) is received to sanitize and reject bad values early (e.g., before calling any path joining or file access logic).

Required changes:

  • Add a new sanitize_path method to validate input path according to the rules mentioned.
  • Update the check_path_exist method so that it first calls sanitize_path and raises/redirects if invalid.
  • Optionally, sanitize params[:path] in the index method before proceeding.

Suggested changeset 1
app/controllers/file_manager_controller.rb

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/app/controllers/file_manager_controller.rb b/app/controllers/file_manager_controller.rb
--- a/app/controllers/file_manager_controller.rb
+++ b/app/controllers/file_manager_controller.rb
@@ -371,6 +371,9 @@
   end
 
   def check_path_exist(path)
+    unless sanitize_path(path)
+      raise ActionController::RoutingError, 'Invalid or unsafe path'
+    end
     @absolute_path = safe_expand_path(path)
     @relative_path = path
     raise ActionController::RoutingError, 'Not Found' unless File.exist?(@absolute_path)
@@ -378,6 +381,20 @@
     @absolute_path
   end
 
+
+  # Only allow relative paths with safe filename components (no traversal, no separators, no multiple dots)
+  def sanitize_path(path)
+    return true if path.nil? || path == ""
+    # Disallow absolute paths
+    return false if path.start_with?('/', '\\')
+    # Disallow directory traversal, multiple dots, or dir separators
+    return false if path.include?("..") || path.include?("/") || path.include?("\\")
+    # Whitelist: only allow simple filenames or relative paths without any dangerous characters
+    allowed_pattern = /\A[\w\-\.]+\z/
+    return false unless path.match?(allowed_pattern)
+    true
+  end
+
   def is_instructor_of_any_course
     current_user_id = current_user.id
     cuds = CourseUserDatum.where(user_id: current_user_id, instructor: true)
EOF
@@ -371,6 +371,9 @@
end

def check_path_exist(path)
unless sanitize_path(path)
raise ActionController::RoutingError, 'Invalid or unsafe path'
end
@absolute_path = safe_expand_path(path)
@relative_path = path
raise ActionController::RoutingError, 'Not Found' unless File.exist?(@absolute_path)
@@ -378,6 +381,20 @@
@absolute_path
end


# Only allow relative paths with safe filename components (no traversal, no separators, no multiple dots)
def sanitize_path(path)
return true if path.nil? || path == ""
# Disallow absolute paths
return false if path.start_with?('/', '\\')
# Disallow directory traversal, multiple dots, or dir separators
return false if path.include?("..") || path.include?("/") || path.include?("\\")
# Whitelist: only allow simple filenames or relative paths without any dangerous characters
allowed_pattern = /\A[\w\-\.]+\z/
return false unless path.match?(allowed_pattern)
true
end

def is_instructor_of_any_course
current_user_id = current_user.id
cuds = CourseUserDatum.where(user_id: current_user_id, instructor: true)
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
end
end
else
File.open(tar_path, 'rb') do |file|

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 4 days ago

To fix this vulnerability, explicit validation and sanitization should be applied directly to user-supplied path values before they are used in any filesystem operations. For archives, both the tar file itself (params[:tar_path]) and the internal file path to extract from the archive (params[:file_path]) must be validated.

Steps to fix:

  1. Define a whitelist pattern for names allowed (disallowing "..", "/", "", or leading ".").
  2. Create a helper method (e.g., sanitize_path_component) to validate/sanitize a path component.
  3. Use this method to validate both the tar file path and the file inside the tar before calling check_path_exist and before passing the paths to extract_single_file_from_tar.
  4. Raise an error or return an appropriate response if the file path fails validation.

Specifics:

  • Edit the relevant controller action to validate/sanitize user input from params[:tar_path] and params[:file_path] before using them.
  • Add a new method to sanitize filenames.
  • Call this method before any file operation that uses a user-supplied filename or path.
  • No changes needed to any business logic; only the validation of inputs before use.
  • Add a test for sanitization to avoid using dangerous filenames.

Suggested changeset 1
app/controllers/file_manager_controller.rb

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/app/controllers/file_manager_controller.rb b/app/controllers/file_manager_controller.rb
--- a/app/controllers/file_manager_controller.rb
+++ b/app/controllers/file_manager_controller.rb
@@ -265,8 +265,8 @@
   end
 
   def view_tar_file
-    tar_path = params[:tar_path]
-    file_path = params[:file_path]
+    tar_path = sanitize_path_component(params[:tar_path])
+    file_path = sanitize_path_component(params[:file_path])
 
     absolute_tar_path = check_path_exist(tar_path)
 
@@ -315,6 +315,23 @@
 
 private
 
+  # Sanitizes a user-supplied file name/path fragment to prevent traversal or absolute paths.
+  # Allow only safe names: alphanumerics, hyphens, underscores, dots (not at start), but not "..", "/", "\", or leading dot.
+  def sanitize_path_component(name)
+    if name.nil? || name.empty?
+      raise ActionController::RoutingError, 'Invalid filename'
+    end
+    # Disallow directory separators or parent references
+    if name.include?('/') || name.include?('\\') || name.include?('..') || name[0] == '.'
+      raise ActionController::RoutingError, 'Invalid filename'
+    end
+    # Only allow whitelisted characters
+    unless name =~ /\A[\w\-.]+\z/
+      raise ActionController::RoutingError, 'Invalid filename'
+    end
+    name
+  end
+
   def populate_directory(current_directory, current_url)
     directory = Dir.entries(current_directory)
     new_url = current_url == '/' ? '' : current_url
EOF
@@ -265,8 +265,8 @@
end

def view_tar_file
tar_path = params[:tar_path]
file_path = params[:file_path]
tar_path = sanitize_path_component(params[:tar_path])
file_path = sanitize_path_component(params[:file_path])

absolute_tar_path = check_path_exist(tar_path)

@@ -315,6 +315,23 @@

private

# Sanitizes a user-supplied file name/path fragment to prevent traversal or absolute paths.
# Allow only safe names: alphanumerics, hyphens, underscores, dots (not at start), but not "..", "/", "\", or leading dot.
def sanitize_path_component(name)
if name.nil? || name.empty?
raise ActionController::RoutingError, 'Invalid filename'
end
# Disallow directory separators or parent references
if name.include?('/') || name.include?('\\') || name.include?('..') || name[0] == '.'
raise ActionController::RoutingError, 'Invalid filename'
end
# Only allow whitelisted characters
unless name =~ /\A[\w\-.]+\z/
raise ActionController::RoutingError, 'Invalid filename'
end
name
end

def populate_directory(current_directory, current_url)
directory = Dir.entries(current_directory)
new_url = current_url == '/' ? '' : current_url
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (2)
app/controllers/file_manager_controller.rb (2)

114-131: Critical performance issue: Inefficient record iteration.

Both find_by_directory_path and find_by_folder_path use Course.find and Assessment.find, which load all records into memory and iterate through them in Ruby. This is highly inefficient and will cause performance issues with many courses/assessments.

Use database queries instead:

 def find_by_directory_path(absolute_path)
-  # Normalize the path to ensure consistent comparison
   normalized_path = Pathname.new(absolute_path).cleanpath.to_s
-  # Find course where the directory_path matches the given absolute path
-  Course.find do |course|
-    course.directory_path.to_s == normalized_path
-  end
+  # Extract course name from path structure
+  course_name = normalized_path.sub("#{BASE_DIRECTORY}/", '').split('/').first
+  Course.find_by(name: course_name) if course_name
 end
 
 def find_by_folder_path(absolute_path)
   normalized_path = Pathname.new(absolute_path).cleanpath.to_s
-
-  # Find assessment where the folder_path matches the given absolute path
-  Assessment.find do |assessment|
-    assessment.folder_path.to_s == normalized_path
-  end
+  # Extract course and assessment names from path
+  parts = normalized_path.sub("#{BASE_DIRECTORY}/", '').split('/')
+  return nil unless parts.length >= 2
+  
+  course = Course.find_by(name: parts[0])
+  course&.assessments&.find_by(name: parts[1])
 end

176-181: Incorrect method signature for tar generation.

Line 177 calls generate_tar(course), but based on the code snippets, Course#generate_tar is an instance method that expects an export_configs array parameter, not a course object.

Fix the method call:

 if course.present?
-  tar_course = generate_tar(course)
+  tar_course = course.generate_tar(['assessments', 'metrics_config'])
   send_data tar_course.string.force_encoding("binary"),
             filename: "#{course.name}.tar",
             type: "application/x-tar",
             disposition: "attachment"
🧹 Nitpick comments (3)
app/views/file_manager/tar_preview.html.erb (1)

77-127: Consider extracting inline styles to the stylesheet.

While inline styles work, consider moving these to app/assets/stylesheets/file_manager.scss for better maintainability and to avoid style duplication across views.

app/controllers/file_manager_controller.rb (2)

133-139: Consider explicit boolean return values.

These methods are named as predicates (with ?) but return the record or nil. For clarity and consistency with Ruby conventions, consider returning explicit booleans.

 def path_belongs_to_assessment?(absolute_path)
-  find_by_folder_path(absolute_path)
+  find_by_folder_path(absolute_path).present?
 end
 
 def path_belongs_to_course?(absolute_path)
-  find_by_directory_path(absolute_path)
+  find_by_directory_path(absolute_path).present?
 end

413-491: LGTM! Tar content listing with proper safety checks.

The method includes crucial path traversal protection (skipping entries with .. or starting with /) and handles both compressed and uncompressed tar formats with appropriate error handling.

Optional: Consider extracting the common entry-processing logic to reduce duplication between the gzip and non-gzip branches.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ba9e925 and f253932.

📒 Files selected for processing (7)
  • app/assets/javascripts/file_manager.js (8 hunks)
  • app/assets/stylesheets/file_manager.scss (1 hunks)
  • app/controllers/file_manager_controller.rb (5 hunks)
  • app/views/file_manager/index.html.erb (2 hunks)
  • app/views/file_manager/tar_file_view.html.erb (1 hunks)
  • app/views/file_manager/tar_preview.html.erb (1 hunks)
  • config/routes.rb (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • app/assets/stylesheets/file_manager.scss
  • app/assets/javascripts/file_manager.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/views/file_manager/index.html.erb
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2024-06-10T19:38:42.782Z
Learnt from: najclark
Repo: autolab/Autolab PR: 2011
File: app/controllers/courses_controller.rb:1316-1320
Timestamp: 2024-06-10T19:38:42.782Z
Learning: The user confirmed the concern about potential security risks when setting `flash[:error]` and `flash[:html_safe]` with user-supplied input, specifically mentioning `course_name` derived from a tar file's contents. It's important to ensure proper sanitization of user-supplied input to prevent XSS vulnerabilities, especially when `flash[:html_safe]` is used, which indicates the content should not be escaped.

Applied to files:

  • app/controllers/file_manager_controller.rb
📚 Learning: 2024-09-20T20:46:52.775Z
Learnt from: evanyeyeye
Repo: autolab/Autolab PR: 2209
File: app/controllers/autograders_controller.rb:0-0
Timestamp: 2024-09-20T20:46:52.775Z
Learning: In the `AutogradersController`, ensure that the `download_file` method has `action_auth_level :download_file, :instructor` to restrict access to instructors.

Applied to files:

  • app/controllers/file_manager_controller.rb
📚 Learning: 2024-11-13T17:02:14.676Z
Learnt from: jhs-panda
Repo: autolab/Autolab PR: 2214
File: app/controllers/schedulers_controller.rb:0-0
Timestamp: 2024-11-13T17:02:14.676Z
Learning: In the `validate_compile_action_file` method in `app/controllers/schedulers_controller.rb`, adding a check to ensure that `action_path` starts with `Rails.root.to_s` is unnecessary.

Applied to files:

  • app/controllers/file_manager_controller.rb
📚 Learning: 2024-09-19T06:35:55.985Z
Learnt from: KesterTan
Repo: autolab/Autolab PR: 2209
File: app/controllers/autograders_controller.rb:32-36
Timestamp: 2024-09-19T06:35:55.985Z
Learning: In the `AutogradersController#edit` method, passing the file paths to the view is necessary to send the files in `download_file`.

Applied to files:

  • app/controllers/file_manager_controller.rb
🧬 Code graph analysis (1)
app/controllers/file_manager_controller.rb (2)
app/models/assessment.rb (3)
  • path (505-507)
  • folder_path (93-95)
  • load_dir_to_tar (467-490)
app/models/course.rb (2)
  • directory_path (52-54)
  • generate_tar (340-378)
🪛 ast-grep (0.39.7)
app/controllers/file_manager_controller.rb

[warning] 187-187: Found the use of an hardcoded passphrase for RSA. The passphrase can be easily discovered, and therefore should not be stored in source-code. It is recommended to remove the passphrase from source-code, and use system environment variables or a restricted configuration file.
Context: assessment.load_dir_to_tar(absolute_path.to_s, "", tar_assessment, filter, "")
Note: [CWE-798]: Use of Hard-coded Credentials [OWASP A07:2021]: Identification and Authentication Failures [REFERENCES]
https://cwe.mitre.org/data/definitions/522.html

(hardcoded-secret-rsa-passphrase-ruby)

🪛 Brakeman (7.1.0)
app/controllers/file_manager_controller.rb

[weak] 198-198: Parameter value used in file name
Type: File Access
Confidence: Weak
More info: https://brakemanscanner.org/docs/warning_types/file_access/

(File Access)

🪛 GitHub Check: CodeQL
app/controllers/file_manager_controller.rb

[failure] 453-453: Uncontrolled data used in path expression
This path depends on a user-provided value.


[failure] 507-507: Uncontrolled data used in path expression
This path depends on a user-provided value.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: test
  • GitHub Check: lint
🔇 Additional comments (9)
config/routes.rb (1)

75-76: LGTM! Route addition follows Rails conventions.

The new route for tar file viewing is properly configured with a named route helper that's used in the related views.

app/views/file_manager/tar_preview.html.erb (2)

40-66: LGTM! Robust handling of tar entry metadata.

The template properly handles both epoch timestamps and Time objects for modification times, and safely renders tar entry metadata with appropriate escaping.


12-14: The edge case concern is not substantiated by the code structure.

After analyzing the controller logic: @path cannot be empty in the tar_preview view because the code at line 30–31 checks (path == "" && is_instructor_of_any_course) first and renders the directory view instead. The tar_preview block is only reached when handling actual files, so @path will always have a value.

Additionally, the codebase already uses File.dirname(path) without special handling in similar contexts (line 43), indicating this pattern is an established convention. The File.dirname(".") case—if a tar file exists at the root level—would follow the same behavior as existing code.

No actionable issues found.

app/views/file_manager/tar_file_view.html.erb (2)

34-36: LGTM! Safe file content rendering.

The file content is properly escaped by ERB and displayed within appropriate semantic HTML elements for code display.


29-32: This review comment is incorrect and can be dismissed.

The code at line 281 already uses File.dirname(tar_path) in a redirect within the same method, and this pattern is working correctly. Additionally, the tar_preview.html.erb template uses an identical File.dirname(@path) pattern for navigation, confirming this is an established, proven approach in the codebase. Path validation occurs at line 271 with check_path_exist(tar_path) before @tar_path is ever set, ensuring only valid paths reach the view. File.dirname() navigating to the parent directory is the intended behavior, not a navigation break.

Likely an incorrect or invalid review comment.

app/controllers/file_manager_controller.rb (4)

35-44: LGTM! Well-structured tar preview logic.

The tar preview flow properly validates the file type, handles errors gracefully, and ensures authorization before displaying archive contents.


141-166: LGTM! Robust directory-to-tar implementation.

The method properly handles files, directories, and symlinks, preserves permissions, and correctly recurses through subdirectories.


182-195: LGTM! Assessment tar generation is correct.

The assessment tar creation properly uses the Assessment model's methods for serialization. Note: The static analysis warning about a "hardcoded passphrase" on line 187 is a false positive—the empty string is a path parameter, not a security credential.


350-354: LGTM! Proper permissions display with error handling.

The permissions field correctly formats file modes as octal strings and gracefully handles errors with a fallback value.

Comment on lines +267 to +314
def view_tar_file
tar_path = params[:tar_path]
file_path = params[:file_path]

absolute_tar_path = check_path_exist(tar_path)

unless check_instructor(absolute_tar_path)
flash[:error] = "You are not authorized to view this path"
redirect_to root_path
return
end

unless is_tar_file?(absolute_tar_path)
flash[:error] = "Not a valid tar file"
redirect_to file_manager_index_path(path: File.dirname(tar_path))
return
end

begin
file_content = extract_single_file_from_tar(absolute_tar_path, file_path)

if file_content.nil?
flash[:error] = "File not found in archive"
redirect_to file_manager_index_path(path: "#{tar_path}?preview=true")
return
end

is_binary = file_content.bytes.take(8192).any? { |byte|
byte.zero? || (byte < 32 && ![9, 10, 13].include?(byte))
}

if is_binary || file_content.bytesize > 1_000_000
send_data file_content,
filename: File.basename(file_path),
disposition: 'attachment'
else
@tar_path = tar_path
@file_path = file_path
@file = file_content.force_encoding('UTF-8')
@tar_file_name = File.basename(absolute_tar_path)
@file_name = file_path
render :tar_file_view, formats: :html
end
rescue StandardError => e
flash[:error] = "Unable to extract file: #{e.message}"
redirect_to file_manager_index_path(path: "#{tar_path}?preview=true")
end
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Path traversal check needed before extraction.

The file_path parameter from user input is passed directly to extract_single_file_from_tar without validation. Unlike list_tar_contents (which skips entries with .. or starting with /), the extraction method lacks these safety checks, potentially allowing extraction of files outside the intended archive structure.

Add path traversal validation before extraction:

   begin
+    # Validate file_path doesn't contain path traversal attempts
+    if file_path.include?('..') || file_path.start_with?('/')
+      flash[:error] = "Invalid file path"
+      redirect_to file_manager_index_path(path: File.dirname(tar_path))
+      return
+    end
+    
     file_content = extract_single_file_from_tar(absolute_tar_path, file_path)
🤖 Prompt for AI Agents
In app/controllers/file_manager_controller.rb around lines 267 to 314, validate
and sanitize the incoming file_path before calling extract_single_file_from_tar
to prevent path traversal: reject or handle any file_path that contains ".."
segments, begins with "/", or is otherwise absolute (also reject patterns like
"%2e%2e" if you decode input), normalize the path (e.g. remove leading slashes
and collapse "../" sequences) and if invalid set flash[:error] and redirect
exactly like the other checks; only pass the sanitized, relative member path
into extract_single_file_from_tar and keep current behavior for valid files.

Comment on lines +493 to +519
def extract_single_file_from_tar(tar_path, file_path)
if File.extname(tar_path).downcase.in?(['.gz', '.tgz']) ||
tar_path.to_s.downcase.end_with?('.tar.gz')
require 'zlib'
Zlib::GzipReader.open(tar_path) do |gz|
tar_reader = Gem::Package::TarReader.new(gz)
tar_reader.rewind
tar_reader.each do |entry|
if entry.full_name == file_path && entry.file?
return entry.read
end
end
end
else
File.open(tar_path, 'rb') do |file|
tar_reader = Gem::Package::TarReader.new(file)
tar_reader.rewind
tar_reader.each do |entry|
if entry.full_name == file_path && entry.file?
return entry.read
end
end
end
end

nil
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Add path traversal protection to extraction method.

Unlike list_tar_contents, this method lacks validation for malicious paths. Add the same safety checks to prevent extracting files with path traversal sequences.

Add path traversal checks before extraction:

 def extract_single_file_from_tar(tar_path, file_path)
+  # Prevent path traversal attacks
+  if file_path.include?('..') || file_path.start_with?('/')
+    raise ArgumentError, "Invalid file path: contains path traversal"
+  end
+  
   if File.extname(tar_path).downcase.in?(['.gz', '.tgz']) ||
      tar_path.to_s.downcase.end_with?('.tar.gz')
     require 'zlib'
     Zlib::GzipReader.open(tar_path) do |gz|
       tar_reader = Gem::Package::TarReader.new(gz)
       tar_reader.rewind
       tar_reader.each do |entry|
+        # Additional check on tar entry itself
+        next if entry.full_name.include?('..') || entry.full_name.start_with?('/')
         if entry.full_name == file_path && entry.file?
           return entry.read
         end
       end
     end
   else
     File.open(tar_path, 'rb') do |file|
       tar_reader = Gem::Package::TarReader.new(file)
       tar_reader.rewind
       tar_reader.each do |entry|
+        # Additional check on tar entry itself
+        next if entry.full_name.include?('..') || entry.full_name.start_with?('/')
         if entry.full_name == file_path && entry.file?
           return entry.read
         end
       end
     end
   end
 
   nil
 end
🧰 Tools
🪛 GitHub Check: CodeQL

[failure] 507-507: Uncontrolled data used in path expression
This path depends on a user-provided value.

🤖 Prompt for AI Agents
In app/controllers/file_manager_controller.rb around lines 493 to 519, the
extract_single_file_from_tar method is missing path traversal protection; add
the same safety checks used in list_tar_contents to validate each entry before
reading it: skip any entry whose full_name starts with a '/' or contains '..'
segments, normalize the joined path (File.expand_path(File.join(base_dir,
entry.full_name))) and ensure it begins with the intended base extraction
directory, and only then allow reading/returning the entry; apply these checks
in both the Gzip and non-Gzip branches and return nil or skip the entry when
validation fails.

Comment on lines +7 to +16
<% content_for :javascripts do %>
<%= javascript_include_tag "highlight.pack" %>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('pre code').forEach((block) => {
hljs.highlightBlock(block);
});
});
</script>
<% end %>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Use modern highlight.js API.

hljs.highlightBlock() is deprecated in highlight.js v10+. Use hljs.highlightElement() instead for compatibility with current versions.

Apply this diff:

   <script>
     document.addEventListener('DOMContentLoaded', function() {
       document.querySelectorAll('pre code').forEach((block) => {
-        hljs.highlightBlock(block);
+        hljs.highlightElement(block);
       });
     });
   </script>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<% content_for :javascripts do %>
<%= javascript_include_tag "highlight.pack" %>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('pre code').forEach((block) => {
hljs.highlightBlock(block);
});
});
</script>
<% end %>
<% content_for :javascripts do %>
<%= javascript_include_tag "highlight.pack" %>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('pre code').forEach((block) => {
hljs.highlightElement(block);
});
});
</script>
<% end %>
🤖 Prompt for AI Agents
In app/views/file_manager/tar_file_view.html.erb around lines 7 to 16, the code
uses the deprecated hljs.highlightBlock(block); call; update the DOM-ready code
to call hljs.highlightElement(block) instead (keeping the querySelectorAll('pre
code') loop and event listener intact) so it uses the modern highlight.js API
and remains compatible with v10+.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants