fix: sécuriser upload auto-declaration
This commit is contained in:
parent
1144180141
commit
15646af9d6
2 changed files with 92 additions and 14 deletions
|
|
@ -29,6 +29,12 @@ const FILE_TYPE_NAMES = {
|
|||
autre: "autre"
|
||||
};
|
||||
|
||||
// Nettoie les valeurs destinées aux métadonnées HTTP (ASCII visible uniquement)
|
||||
const sanitizeMetadataValue = (value: string) =>
|
||||
(value || "")
|
||||
.replace(/[^\x20-\x7E]+/g, "") // retire caractères non ASCII imprimables et contrôles
|
||||
.slice(0, 100); // limite de sécurité
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const formData = await req.formData();
|
||||
|
|
@ -36,7 +42,16 @@ export async function POST(req: NextRequest) {
|
|||
const token = formData.get('token') as string; // Changé de 'matricule' à 'token'
|
||||
const type = formData.get('type') as string;
|
||||
|
||||
console.log('[AUTO-DECLARATION-API] Réception fichier:', {
|
||||
fileName: file?.name,
|
||||
fileSize: file?.size,
|
||||
fileType: file?.type,
|
||||
type,
|
||||
hasToken: !!token
|
||||
});
|
||||
|
||||
if (!file) {
|
||||
console.error('[AUTO-DECLARATION-API] Aucun fichier fourni');
|
||||
return NextResponse.json({ error: "Aucun fichier fourni" }, { status: 400 });
|
||||
}
|
||||
|
||||
|
|
@ -77,12 +92,34 @@ export async function POST(req: NextRequest) {
|
|||
return NextResponse.json({ error: "Matricule du salarié non trouvé" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Vérifier le type de fichier
|
||||
if (!ALLOWED_FILE_TYPES[file.type as keyof typeof ALLOWED_FILE_TYPES]) {
|
||||
// Vérifier le type de fichier (type MIME ou extension)
|
||||
const fileExtension = file.name.split('.').pop()?.toLowerCase() || '';
|
||||
const allowedExtensions = ['pdf', 'jpg', 'jpeg', 'png'];
|
||||
|
||||
const isValidMimeType = ALLOWED_FILE_TYPES[file.type as keyof typeof ALLOWED_FILE_TYPES];
|
||||
const isValidExtension = allowedExtensions.includes(fileExtension);
|
||||
|
||||
if (!isValidMimeType && !isValidExtension) {
|
||||
console.error('[AUTO-DECLARATION-API] Type de fichier non autorisé:', {
|
||||
receivedType: file.type,
|
||||
fileName: file.name,
|
||||
fileExtension,
|
||||
allowedTypes: Object.keys(ALLOWED_FILE_TYPES),
|
||||
allowedExtensions
|
||||
});
|
||||
return NextResponse.json({
|
||||
error: "Type de fichier non autorisé. Seuls les fichiers PDF, JPG et PNG sont acceptés"
|
||||
error: `Type de fichier non autorisé (${file.type || 'inconnu'}, extension: .${fileExtension}). Seuls les fichiers PDF, JPG et PNG sont acceptés`
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// Log si on accepte par extension mais pas par MIME type
|
||||
if (!isValidMimeType && isValidExtension) {
|
||||
console.warn('[AUTO-DECLARATION-API] Fichier accepté par extension mais type MIME invalide:', {
|
||||
receivedType: file.type,
|
||||
fileName: file.name,
|
||||
fileExtension
|
||||
});
|
||||
}
|
||||
|
||||
// Vérifier la taille du fichier
|
||||
const maxSize = ALLOWED_FILE_TYPES[file.type as keyof typeof ALLOWED_FILE_TYPES];
|
||||
|
|
@ -93,10 +130,19 @@ export async function POST(req: NextRequest) {
|
|||
}
|
||||
|
||||
// Générer un nom de fichier unique
|
||||
const fileExtension = file.name.split('.').pop() || '';
|
||||
const fileName = `${FILE_TYPE_NAMES[type as keyof typeof FILE_TYPE_NAMES]}-${Date.now()}.${fileExtension}`;
|
||||
const rawExtension = file.name.split('.').pop() || '';
|
||||
const safeExtension = rawExtension.replace(/[^a-zA-Z0-9]/g, '') || 'bin';
|
||||
const fileName = `${FILE_TYPE_NAMES[type as keyof typeof FILE_TYPE_NAMES]}-${Date.now()}.${safeExtension}`;
|
||||
const s3Key = `justif-salaries/${matricule}/${fileName}`;
|
||||
|
||||
const sanitizedOriginalName = sanitizeMetadataValue(file.name) || 'file';
|
||||
if (sanitizedOriginalName !== file.name) {
|
||||
console.warn('[AUTO-DECLARATION-API] Nom de fichier nettoyé pour les métadonnées S3', {
|
||||
original: file.name,
|
||||
sanitized: sanitizedOriginalName
|
||||
});
|
||||
}
|
||||
|
||||
// Convertir le fichier en buffer
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
|
||||
|
|
@ -109,13 +155,20 @@ export async function POST(req: NextRequest) {
|
|||
Metadata: {
|
||||
'matricule': matricule,
|
||||
'type': type,
|
||||
'original-filename': file.name,
|
||||
'original-filename': sanitizedOriginalName,
|
||||
'uploaded-at': new Date().toISOString()
|
||||
}
|
||||
});
|
||||
|
||||
await s3Client.send(uploadCommand);
|
||||
|
||||
console.log('[AUTO-DECLARATION-API] Upload S3 réussi:', {
|
||||
key: s3Key,
|
||||
matricule,
|
||||
type,
|
||||
fileName
|
||||
});
|
||||
|
||||
// Construire l'URL du fichier
|
||||
const fileUrl = `https://${BUCKET_NAME}.s3.eu-west-3.amazonaws.com/${s3Key}`;
|
||||
|
||||
|
|
|
|||
|
|
@ -164,6 +164,7 @@ interface S3Document {
|
|||
export default function AutoDeclarationPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const token = searchParams.get('token'); // Changé de 'matricule' à 'token'
|
||||
const MAX_UPLOAD_SIZE = 4.5 * 1024 * 1024; // 4.5MB Vercel body limit safety margin
|
||||
|
||||
const [salarieData, setSalarieData] = useState<SalarieData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
|
@ -289,26 +290,44 @@ export default function AutoDeclarationPage() {
|
|||
const handleFileUpload = async (type: keyof FormData, file: File) => {
|
||||
if (!file || !token) return; // Changé de 'matricule' à 'token'
|
||||
|
||||
if (file.size > MAX_UPLOAD_SIZE) {
|
||||
toast.error('Fichier trop volumineux. Limite actuelle : 4.5MB.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[AUTO-DECLARATION] Upload fichier:', {
|
||||
type,
|
||||
fileName: file.name,
|
||||
fileSize: file.size,
|
||||
fileType: file.type,
|
||||
token: token.substring(0, 10) + '...'
|
||||
});
|
||||
|
||||
setUploading(type);
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('token', token); // Changé de 'matricule' à 'token'
|
||||
formData.append('type', type);
|
||||
const uploadFormData = new FormData();
|
||||
uploadFormData.append('file', file);
|
||||
uploadFormData.append('token', token); // Changé de 'matricule' à 'token'
|
||||
uploadFormData.append('type', type);
|
||||
|
||||
const response = await fetch('/api/auto-declaration/upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
body: uploadFormData
|
||||
});
|
||||
|
||||
console.log('[AUTO-DECLARATION] Réponse upload:', response.status, response.statusText);
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.message || 'Erreur lors du téléchargement');
|
||||
console.error('[AUTO-DECLARATION] Erreur upload:', error);
|
||||
throw new Error(error.error || error.message || 'Erreur lors du téléchargement');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
console.log('[AUTO-DECLARATION] Upload réussi:', result);
|
||||
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[type]: file
|
||||
|
|
@ -614,10 +633,16 @@ export default function AutoDeclarationPage() {
|
|||
type="file"
|
||||
id={`file-${type}`}
|
||||
className="hidden"
|
||||
accept=".pdf,.jpg,.jpeg,.png"
|
||||
accept=".pdf,.jpg,.jpeg,.png,image/jpeg,image/png,application/pdf"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
console.log('[CLIENT] Fichier sélectionné:', {
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
fieldType: type
|
||||
});
|
||||
handleFileUpload(type, file);
|
||||
}
|
||||
}}
|
||||
|
|
@ -634,7 +659,7 @@ export default function AutoDeclarationPage() {
|
|||
{isUploading ? 'Téléchargement...' : isDragging ? 'Déposez le fichier ici' : 'Glissez un fichier ou cliquez pour choisir'}
|
||||
</span>
|
||||
<span className="text-xs text-gray-400 mt-1">
|
||||
PDF, JPG, PNG (max 10MB)
|
||||
PDF, JPG, PNG (max 4.5MB)
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
|
|
|
|||
Loading…
Reference in a new issue