Skip to content

Commit aaab9a1

Browse files
authored
fix: local query file reads (#4402)
# Summary Adds directory listing functionality to the Vite bridge, allowing remote clients to list SQL files in the config/queries directory. This enables dynamic discovery of available query files for remote development workflows. ## Changes - New feature: dir:list message handler for listing directory contents - Security validation: Added ValidateDirPath() function with comprehensive path security checks - File filtering: Directory listing returns only .sql files (including .obo.sql), consistent with file read restrictions - Error handling: Proper error handling for JSON marshaling, directory validation, and read operations - Test coverage: Added comprehensive test suite (validate_dir_test.go) with 9 test cases covering security and functionality ## Security Features - ✅ Path traversal prevention (blocks ../../ attempts) - ✅ Directory boundary enforcement (restricts to config/queries only) - ✅ Hidden directory blocking (prevents access to .git, .env, etc.) - ✅ Prefix attack prevention (blocks queries-malicious/ attempts) - ✅ File type filtering (returns only .sql and .obo.sql files) Co-authored-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
1 parent 99dc0b1 commit aaab9a1

File tree

2 files changed

+387
-0
lines changed

2 files changed

+387
-0
lines changed

libs/apps/vite/bridge.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,15 @@ func (vb *Bridge) handleMessage(msg *BridgeMessage) error {
410410
}(*msg)
411411
return nil
412412

413+
case "dir:list":
414+
// Handle directory list requests in parallel
415+
go func(dirListMsg BridgeMessage) {
416+
if err := vb.handleDirListRequest(&dirListMsg); err != nil {
417+
log.Errorf(vb.ctx, "[vite_bridge] Error handling dir list request for %s: %v", dirListMsg.Path, err)
418+
}
419+
}(*msg)
420+
return nil
421+
413422
case "hmr:message":
414423
return vb.handleHMRMessage(msg)
415424

@@ -596,6 +605,88 @@ func (vb *Bridge) handleFileReadRequest(msg *BridgeMessage) error {
596605
return nil
597606
}
598607

608+
func (vb *Bridge) handleDirListRequest(msg *BridgeMessage) error {
609+
log.Debugf(vb.ctx, "[vite_bridge] Dir list request: %s", msg.Path)
610+
611+
if err := ValidateDirPath(msg.Path); err != nil {
612+
log.Warnf(vb.ctx, "[vite_bridge] Dir validation failed for %s: %v", msg.Path, err)
613+
return vb.sendDirListError(msg.RequestID, fmt.Sprintf("Invalid directory path: %v", err))
614+
}
615+
616+
entries, err := os.ReadDir(msg.Path)
617+
618+
response := BridgeMessage{
619+
Type: "dir:list:response",
620+
RequestID: msg.RequestID,
621+
}
622+
623+
if err != nil {
624+
log.Errorf(vb.ctx, "[vite_bridge] Failed to read directory %s: %v", msg.Path, err)
625+
response.Error = err.Error()
626+
} else {
627+
files := make([]string, 0, len(entries))
628+
for _, entry := range entries {
629+
if !entry.IsDir() && filepath.Ext(entry.Name()) == allowedExtension {
630+
files = append(files, entry.Name())
631+
}
632+
}
633+
log.Debugf(vb.ctx, "[vite_bridge] Listed directory %s (%d SQL files)", msg.Path, len(files))
634+
// Client expects files as JSON string in content field
635+
filesJSON, err := json.Marshal(files)
636+
if err != nil {
637+
log.Errorf(vb.ctx, "[vite_bridge] Failed to marshal file list: %v", err)
638+
response.Error = fmt.Sprintf("Failed to marshal file list: %v", err)
639+
} else {
640+
response.Content = string(filesJSON)
641+
}
642+
}
643+
644+
responseData, err := json.Marshal(response)
645+
if err != nil {
646+
return fmt.Errorf("failed to marshal dir list response: %w", err)
647+
}
648+
649+
log.Debugf(vb.ctx, "[vite_bridge] Sending dir list response: %s", string(responseData))
650+
651+
select {
652+
case vb.tunnelWriteChan <- prioritizedMessage{
653+
messageType: websocket.TextMessage,
654+
data: responseData,
655+
priority: 1,
656+
}:
657+
log.Debugf(vb.ctx, "[vite_bridge] Dir list response sent successfully")
658+
case <-time.After(wsWriteTimeout):
659+
return errors.New("timeout sending dir list response")
660+
}
661+
662+
return nil
663+
}
664+
665+
func (vb *Bridge) sendDirListError(requestID, errMsg string) error {
666+
response := BridgeMessage{
667+
Type: "dir:list:response",
668+
RequestID: requestID,
669+
Error: errMsg,
670+
}
671+
672+
responseData, err := json.Marshal(response)
673+
if err != nil {
674+
return fmt.Errorf("failed to marshal dir list error response: %w", err)
675+
}
676+
677+
select {
678+
case vb.tunnelWriteChan <- prioritizedMessage{
679+
messageType: websocket.TextMessage,
680+
data: responseData,
681+
priority: 1,
682+
}:
683+
case <-time.After(wsWriteTimeout):
684+
return errors.New("timeout sending dir list error response")
685+
}
686+
687+
return nil
688+
}
689+
599690
func ValidateFilePath(requestedPath string) error {
600691
// Clean the path to resolve any ../ or ./ components
601692
cleanPath := filepath.Clean(requestedPath)
@@ -635,6 +726,50 @@ func ValidateFilePath(requestedPath string) error {
635726
return nil
636727
}
637728

729+
// ValidateDirPath validates that a directory path is within the allowed directory.
730+
func ValidateDirPath(requestedPath string) error {
731+
// Clean the path to resolve any ../ or ./ components
732+
cleanPath := filepath.Clean(requestedPath)
733+
734+
// Get absolute path
735+
absPath, err := filepath.Abs(cleanPath)
736+
if err != nil {
737+
return fmt.Errorf("failed to resolve absolute path: %w", err)
738+
}
739+
740+
// Get the working directory
741+
cwd, err := os.Getwd()
742+
if err != nil {
743+
return fmt.Errorf("failed to get working directory: %w", err)
744+
}
745+
746+
// Construct the allowed base directory (absolute path)
747+
allowedDir := filepath.Join(cwd, allowedBasePath)
748+
749+
// Ensure the resolved path is within the allowed directory
750+
// Add trailing separator to prevent prefix attacks (e.g., queries-malicious/)
751+
allowedDirWithSep := allowedDir + string(filepath.Separator)
752+
if absPath != allowedDir && !strings.HasPrefix(absPath, allowedDirWithSep) {
753+
return fmt.Errorf("path %s is outside allowed directory %s", absPath, allowedBasePath)
754+
}
755+
756+
// Additional check: no hidden directories
757+
if strings.HasPrefix(filepath.Base(absPath), ".") {
758+
return errors.New("hidden directories are not allowed")
759+
}
760+
761+
// Verify it's actually a directory
762+
info, err := os.Stat(absPath)
763+
if err != nil {
764+
return fmt.Errorf("failed to stat path: %w", err)
765+
}
766+
if !info.IsDir() {
767+
return fmt.Errorf("path %s is not a directory", requestedPath)
768+
}
769+
770+
return nil
771+
}
772+
638773
// Helper to send error response
639774
func (vb *Bridge) sendFileReadError(requestID, errorMsg string) error {
640775
response := BridgeMessage{

0 commit comments

Comments
 (0)