fix: sécuriser upload auto-declaration

This commit is contained in:
odentas 2025-12-09 11:40:09 +01:00
parent 1144180141
commit 15646af9d6
2 changed files with 92 additions and 14 deletions

View file

@ -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}`;

View file

@ -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>