π Files: components/chat/FileUpload.jsx (or equivalent)
hooks/useFileUpload.js
styles/ β upload progress UI
π¨ Summary
NPMChat's file sharing feature accepts any file of any size with
zero client-side validation. There is:
- β No file type restriction (
.exe, .sh, .bat, .php all accepted)
- β No file size limit (500MB+ uploads start silently)
- β No upload progress indicator (UI appears frozen on large files)
- β No cancel upload option
- β No error message on oversized files
For a developer-focused chat platform, this is a critical security
and UX failure β malicious scripts can be shared freely, and large
file uploads hang the UI with no feedback.
π₯ Reproducible Failures
Failure 1 β Malicious file accepted silently:
Open any chat room in NPMChat
Click file share / attachment icon
Select a file named "backdoor.exe" or "exploit.sh"
β Expected: "File type not allowed" error shown, upload blocked
β Actual: Upload begins immediately, file sent to all chat participants
Failure 2 β Large file freezes UI:
Select a 100MB+ video file
Click send / attach
β Expected: Progress bar shown (0% β 100%), cancel button available
β Actual: UI appears frozen, no feedback, no way to cancel
Browser may become unresponsive
Failure 3 β No feedback on failure:
Select a 500MB file (exceeds server limit)
Upload starts β server rejects with 413 Payload Too Large
β Expected: "File too large. Max 10MB allowed" shown to user
β Actual: Silent failure β upload just stops with no message
β
Proposed Fix β 4-Part Solution
Fix 1 β File validation config
// lib/fileValidation.ts
export const FILE_CONFIG = {
maxSizeMB: 10,
maxSizeBytes: 10 * 1024 * 1024, // 10MB
// Allowed types for a developer chat platform
allowedTypes: [
// Images
"image/jpeg", "image/png", "image/gif", "image/webp", "image/svg+xml",
// Documents
"application/pdf", "text/plain", "text/markdown",
"application/json", "text/csv",
// Code files
"text/javascript", "text/typescript", "text/html", "text/css",
"text/x-python", "text/x-java-source",
// Archives (safe)
"application/zip",
],
// Blocked extensions (double-check beyond MIME type)
blockedExtensions: [
".exe", ".sh", ".bat", ".cmd", ".ps1", ".php",
".py", ".rb", ".pl", ".vbs", ".js", // executables
".dll", ".so", ".dylib", // binaries
],
} as const;
export type ValidationResult =
| { valid: true }
| { valid: false; error: string };
export function validateFile(file: File): ValidationResult {
// Check size
if (file.size > FILE_CONFIG.maxSizeBytes) {
const sizeMB = (file.size / 1024 / 1024).toFixed(1);
return {
valid: false,
error: `File too large (${sizeMB}MB). Maximum allowed size is ${FILE_CONFIG.maxSizeMB}MB.`,
};
}
// Check MIME type
if (!FILE_CONFIG.allowedTypes.includes(file.type as any)) {
return {
valid: false,
error: `File type "${file.type || "unknown"}" is not allowed. Only images, documents, and code files are permitted.`,
};
}
// Check extension (defense in depth)
const ext = "." + file.name.split(".").pop()?.toLowerCase();
if (FILE_CONFIG.blockedExtensions.includes(ext as any)) {
return {
valid: false,
error: `Files with extension "${ext}" are not allowed for security reasons.`,
};
}
return { valid: true };
}
Fix 2 β Upload progress hook
// hooks/useFileUpload.ts
import { useState, useRef, useCallback } from "react";
import { validateFile } from "@/lib/fileValidation";
interface UploadState {
progress: number; // 0-100
status: "idle" | "validating" | "uploading" | "success" | "error" | "cancelled";
error: string | null;
fileName: string | null;
}
export function useFileUpload() {
const [state, setState] = useState<UploadState>({
progress: 0,
status: "idle",
error: null,
fileName: null,
});
const xhrRef = useRef<XMLHttpRequest | null>(null);
const upload = useCallback(async (file: File, roomId: string) => {
// Step 1: Validate
setState({ progress: 0, status: "validating", error: null, fileName: file.name });
const validation = validateFile(file);
if (!validation.valid) {
setState((s) => ({ ...s, status: "error", error: validation.error }));
return;
}
// Step 2: Upload with XHR for progress tracking
setState((s) => ({ ...s, status: "uploading" }));
const formData = new FormData();
formData.append("file", file);
formData.append("roomId", roomId);
const xhr = new XMLHttpRequest();
xhrRef.current = xhr;
return new Promise<void>((resolve, reject) => {
// β
Track progress
xhr.upload.addEventListener("progress", (e) => {
if (e.lengthComputable) {
const pct = Math.round((e.loaded / e.total) * 100);
setState((s) => ({ ...s, progress: pct }));
}
});
xhr.addEventListener("load", () => {
if (xhr.status >= 200 && xhr.status < 300) {
setState((s) => ({ ...s, status: "success", progress: 100 }));
resolve();
} else if (xhr.status === 413) {
setState((s) => ({
...s, status: "error",
error: "File too large. Server rejected the upload.",
}));
reject();
} else {
setState((s) => ({
...s, status: "error",
error: "Upload failed. Please try again.",
}));
reject();
}
});
xhr.addEventListener("error", () => {
setState((s) => ({ ...s, status: "error", error: "Network error during upload." }));
reject();
});
xhr.open("POST", "/api/upload");
xhr.send(formData);
});
}, []);
// β
Cancel in-flight upload
const cancel = useCallback(() => {
if (xhrRef.current) {
xhrRef.current.abort();
xhrRef.current = null;
setState({ progress: 0, status: "cancelled", error: null, fileName: null });
}
}, []);
const reset = useCallback(() => {
setState({ progress: 0, status: "idle", error: null, fileName: null });
}, []);
return { ...state, upload, cancel, reset };
}
Fix 3 β FileUpload UI component with progress + cancel
// components/chat/FileUpload.tsx
import { useRef } from "react";
import { useFileUpload } from "@/hooks/useFileUpload";
import { Paperclip, X, CheckCircle, AlertCircle } from "lucide-react";
interface Props {
roomId: string;
}
export function FileUpload({ roomId }: Props) {
const inputRef = useRef<HTMLInputElement>(null);
const { progress, status, error, fileName, upload, cancel, reset } =
useFileUpload();
const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
await upload(file, roomId);
// Reset input so same file can be re-selected
if (inputRef.current) inputRef.current.value = "";
};
return (
<div className="file-upload-wrapper">
{/* Hidden native input */}
<input
ref={inputRef}
type="file"
className="hidden"
onChange={handleFileSelect}
aria-label="Attach file"
/>
{/* Upload trigger button */}
{status === "idle" || status === "success" || status === "cancelled" ? (
<button
onClick={() => { reset(); inputRef.current?.click(); }}
className="attach-btn"
title="Attach file (max 10MB)"
>
<Paperclip className="w-5 h-5" />
</button>
) : null}
{/* Progress bar β shown during upload */}
{status === "uploading" || status === "validating" ? (
<div className="upload-progress-container">
<span className="upload-filename truncate">{fileName}</span>
<div className="progress-bar-track">
<div
className="progress-bar-fill"
style={{ width: `${progress}%` }}
role="progressbar"
aria-valuenow={progress}
aria-valuemin={0}
aria-valuemax={100}
/>
</div>
<span className="upload-pct">{progress}%</span>
{/* β
Cancel button */}
<button
onClick={cancel}
className="cancel-btn"
title="Cancel upload"
>
<X className="w-4 h-4" />
</button>
</div>
) : null}
{/* Error message */}
{status === "error" && error && (
<div className="upload-error" role="alert">
<AlertCircle className="w-4 h-4 text-red-500" />
<span>{error}</span>
<button onClick={reset} className="text-xs underline ml-2">
Dismiss
</button>
</div>
)}
{/* Success */}
{status === "success" && (
<div className="upload-success">
<CheckCircle className="w-4 h-4 text-green-500" />
<span>File sent!</span>
</div>
)}
</div>
);
}
Fix 4 β Progress bar CSS
/* styles/fileUpload.css */
.upload-progress-container {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
background: var(--bg-secondary, #f5f5f5);
border-radius: 8px;
min-width: 240px;
}
.upload-filename {
font-size: 0.75rem;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.progress-bar-track {
flex: 1;
height: 6px;
background: #ddd;
border-radius: 3px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background: #3b82f6;
border-radius: 3px;
transition: width 0.2s ease;
}
.upload-pct {
font-size: 0.7rem;
color: #666;
min-width: 30px;
}
.upload-error {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
background: #fef2f2;
border: 1px solid #fca5a5;
border-radius: 6px;
font-size: 0.8rem;
color: #dc2626;
}
.upload-success {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.8rem;
color: #16a34a;
}
π Impact Table
| Failure |
Before Fix |
After Fix |
.exe / .sh uploaded |
β
Accepted silently |
β Blocked: "type not allowed" |
| 200MB video upload |
UI freezes silently |
Progress bar + cancel button shown |
| 413 server rejection |
Silent failure |
"File too large" message shown |
| Upload progress |
Unknown (spinner or freeze) |
Live 0β100% progress bar |
| Cancel mid-upload |
Impossible |
β
Cancel button aborts XHR |
| Wrong file type |
No feedback |
Inline error with clear message |
π Files to Add / Modify
| File |
Change |
lib/fileValidation.ts |
New β type/size/extension validator |
hooks/useFileUpload.ts |
New β XHR upload with progress + cancel |
components/chat/FileUpload.tsx |
Replace existing with progress UI |
styles/fileUpload.css |
New β progress bar + error/success styles |
π§ͺ How to Test
Try uploading "test.exe" β blocked: "type not allowed" β
Try uploading a 50MB file β blocked: "File too large (50MB). Max 10MB" β
Upload a valid 2MB PNG β progress bar animates 0β100% β
Start a large upload β click cancel β XHR aborted, "cancelled" shown β
Lose network mid-upload β "Network error during upload" shown β
π Raising this for GSSoC 2025 contribution.
All 4 parts are clearly scoped. useFileUpload hook + validateFile
utility are fully reusable across the app.
Happy to implement if assigned.
Estimated effort: 4β6 hours | Difficulty: Level 2 (Intermediate)
Labels: critical Β· bug Β· security Β· frontend Β· UI/UX Β· level 2 Β· gssoc25 Β· help wanted
π Files:
components/chat/FileUpload.jsx(or equivalent)hooks/useFileUpload.jsstyles/β upload progress UIπ¨ Summary
NPMChat's file sharing feature accepts any file of any size with
zero client-side validation. There is:
.exe,.sh,.bat,.phpall accepted)For a developer-focused chat platform, this is a critical security
and UX failure β malicious scripts can be shared freely, and large
file uploads hang the UI with no feedback.
π₯ Reproducible Failures
Failure 1 β Malicious file accepted silently:
Open any chat room in NPMChat
Click file share / attachment icon
Select a file named "backdoor.exe" or "exploit.sh"
β Expected: "File type not allowed" error shown, upload blocked
β Actual: Upload begins immediately, file sent to all chat participants
Failure 2 β Large file freezes UI:
Select a 100MB+ video file
Click send / attach
β Expected: Progress bar shown (0% β 100%), cancel button available
β Actual: UI appears frozen, no feedback, no way to cancel
Browser may become unresponsive
Failure 3 β No feedback on failure:
Select a 500MB file (exceeds server limit)
Upload starts β server rejects with 413 Payload Too Large
β Expected: "File too large. Max 10MB allowed" shown to user
β Actual: Silent failure β upload just stops with no message
β Proposed Fix β 4-Part Solution
Fix 1 β File validation config
Fix 2 β Upload progress hook
Fix 3 β FileUpload UI component with progress + cancel
Fix 4 β Progress bar CSS
π Impact Table
.exe/.shuploadedπ Files to Add / Modify
lib/fileValidation.tshooks/useFileUpload.tscomponents/chat/FileUpload.tsxstyles/fileUpload.cssπ§ͺ How to Test
Try uploading "test.exe" β blocked: "type not allowed" β
Try uploading a 50MB file β blocked: "File too large (50MB). Max 10MB" β
Upload a valid 2MB PNG β progress bar animates 0β100% β
Start a large upload β click cancel β XHR aborted, "cancelled" shown β
Lose network mid-upload β "Network error during upload" shown β