{"record":{"id":"e99b6b7483643bbf","repo":"nextcloud/server","slug":"no-hash-provided","errorCode":null,"errorMessage":"No hash provided.","messagePattern":"No hash provided\\.","errorType":"exception","errorClass":"Sabre\\DAV\\Exception\\BadRequest","httpStatus":400,"severity":"error","filePath":"apps/dav/lib/BulkUpload/MultipartRequestParser.php","lineNumber":242,"sourceCode":"\n\t\t// Read '\\r\\n'.\n\t\tstream_get_contents($this->stream, 2);\n\n\t\treturn $content;\n\t}\n\n\t/**\n\t * Compute the MD5 or checksum hash of the next x bytes.\n\t * TODO: Drop $md5 argument when the latest desktop client that uses it is no longer supported.\n\t */\n\tprivate function validateHash(int $length, string $fileMd5Header, string $checksumHeader): void {\n\t\tif ($checksumHeader !== '') {\n\t\t\t[$algorithm, $hash] = explode(':', $checksumHeader, 2);\n\t\t} elseif ($fileMd5Header !== '') {\n\t\t\t$algorithm = 'md5';\n\t\t\t$hash = $fileMd5Header;\n\t\t} else {\n\t\t\tthrow new BadRequest('No hash provided.');\n\t\t}\n\n\t\t$context = hash_init($algorithm);\n\t\thash_update_stream($context, $this->stream, $length);\n\t\tfseek($this->stream, -$length, SEEK_CUR);\n\t\t$computedHash = hash_final($context);\n\t\tif ($hash !== $computedHash) {\n\t\t\tthrow new BadRequest(\"Computed $algorithm hash is incorrect ($computedHash).\");\n\t\t}\n\t}\n}\n","sourceCodeStart":224,"sourceCodeEnd":254,"githubUrl":"https://github.com/nextcloud/server/blob/ecdeb153ffdf227235c9a7e2d13dbe0f9c817bc3/apps/dav/lib/BulkUpload/MultipartRequestParser.php#L224-L254","documentation":"validateHash() needs either oc-checksum ('algo:hash') or x-file-md5 with a non-empty value. Reaching its else-branch means both were passed as empty strings - which happens when readPartHeaders()' isset() checks pass because the headers exist but hold empty values ('X-File-MD5:' with nothing after the colon).","triggerScenarios":"A bulk-upload part with a hash header that is present but blank, e.g. 'X-File-MD5:\\r\\n' or 'OC-Checksum: \\r\\n'; isset() in readPartHeaders() lets it through, then validateHash() rejects it with HTTP 400.","commonSituations":"Template code emitting header names with unfilled placeholders; clients sending the header name unconditionally and only filling it when a checksum was computed.","solutions":["Always compute and send a real digest - the endpoint requires integrity verification, an empty value is as bad as none","Only build the header line after the hash value is known, never emit a bare 'Header:' placeholder","If you control a buggy middleware stripping header values, fix it there"],"exampleFix":"// before\n\"X-File-MD5: $maybeUndefinedVar\\r\\n\" // emits 'X-File-MD5: ' when unset\n\n// after\n\"X-File-MD5: \" . hash('md5', $content) . \"\\r\\n\"","handlingStrategy":"validation","validationCode":"// Never emit an empty hash header\nif (!isset($md5) || $md5 === '') {\n    $md5 = hash('md5', $content); // compute instead of sending blank\n}\n$partHeader = \"X-File-MD5: {$md5}\\r\\n\";","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Omit-or-fill: never send a hash header with an empty value","Compute the digest at payload-build time, right after the content bytes are final","Note the server only checks isset() at first, so blank values fail later with a different message"],"tags":["php","webdav","nextcloud","bulk-upload","multipart","checksum","empty-header"],"backgroundTag":"missing-checksum-header","analyzedSha":"ecdeb153ffdf227235c9a7e2d13dbe0f9c817bc3","analyzedAt":"2026-08-17T01:36:13.386Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}