DEV Community

stale_orbit
stale_orbit

Posted on

Why NocoBase rejects your .doc and .xls uploads

NocoBase file storage lets you restrict uploads to a list of allowed MIME types. Someone on the official forum filled that list in correctly — application/pdf, application/msword, application/vnd.ms-excel, and the two OOXML types — and found that legacy Word and Excel files were still refused with "Mime type not allowed by storage rule" (t/13553).

The thread is instructive for how it unfolds. A related report (t/13544) surfaces a red herring along the way — a space after a comma supposedly breaking the list — and after fixing that, PDFs go through while .doc and .xls still don't. Official staff asked for a reproducible case. Nobody produced one. The reporter gave up and set the storage to allow every format.

Plenty of Japanese and Chinese enterprises still run on .doc and .xls, so "allow everything" is not a satisfying place to land. I measured it on 2.1.23, and the cause turns out to have nothing to do with how the MIME list is written.

Test setup: NocoBase 2.1.23 (official Docker image) + PostgreSQL 16, uploads driven through the REST API. Where behavior needed explaining I read the implementation in @nocobase/plugin-file-manager.

The short version

NocoBase inspects the file's contents to determine its MIME type. The Content-Type your browser announces is not used. The filename extension is used only when the contents can't be identified.

And by content, a legacy Office file is application/x-cfb — not application/msword, not application/vnd.ms-excel. Your allowlist never had a chance to match.

The same reasoning applies to modern Office files, which come out as application/zip. The forum reporter was focused on .doc and .xls; .docx was failing too, unnoticed.

What actually gets detected

With restrictions off, here is what NocoBase recorded for each upload:

Uploaded file Recorded mimetype
.doc (legacy Word) application/x-cfb
.xls (legacy Excel) application/x-cfb
.docx (modern Word) application/zip
.pdf application/pdf

Legacy Word and Excel land on the same value because they are the same container — OLE2 Compound File Binary. The first bytes give it away:

legacy.doc  first 8 bytes: d0cf11e0a1b11ae1
legacy.xls  first 8 bytes: d0cf11e0a1b11ae1   ← identical
Enter fullscreen mode Exit fullscreen mode

Modern Office files are ZIP archives, hence application/zip.

Reproducing the forum case

Setting the exact allowlist from the thread:

application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
Enter fullscreen mode Exit fullscreen mode
File Result
.doc 400 Mime type not allowed by storage rule
.xls 400
.docx 400
.pdf 200

Only PDF survives. Office files fail across the board, old and new.

The allowlist that works

Put the detected values in:

application/pdf,application/x-cfb                      # legacy Office
application/pdf,application/x-cfb,application/zip      # plus modern Office
Enter fullscreen mode Exit fullscreen mode

Both .doc and .xls upload successfully with that.

Why: the implementation

Upload handling peeks at the first 4100 bytes, hands them to the file-type package, and overwrites the mimetype with whatever comes back.

// @nocobase/plugin-file-manager, attachments.js (condensed)
const peekSize = 4100;

const validate = async (header) => {
  const { fileTypeFromBuffer } = await import('file-type');
  const type = await fileTypeFromBuffer(new Uint8Array(header));
  if (type) {
    detectedMime = type.mime;                              // from content
  } else {
    const fromFilename = mimeTypes.lookup(file.originalname);
    if (fromFilename) {
      detectedMime = fromFilename;                         // fallback: extension
    }
  }
  if (!detectedMime || !matchesMimePattern(detectedMime, pattern) || ...) {
    const err = new Error('Mime type not allowed by storage rule');
    ...
  }
};
Enter fullscreen mode Exit fullscreen mode

Content wins when it can be identified; the extension is consulted only as a fallback. That asymmetry is observable:

File Contents Extension Recorded mimetype
fake.doc plain text .doc application/msword (extension used)
disguised.txt OLE2 .txt application/x-cfb (contents used)

file-type recognizes binary signatures, so text files fall through to the extension. In practice: a binary with a lying extension gets caught; a text file is taken at its word.

About that comma-and-space theory

The related thread suggested that a space after a comma breaks the list. It doesn't, on 2.1.23 — each entry is trimmed:

return normalizedPattern.split(',').map((item) => item.trim()).filter(Boolean).some(mimeMatch(mimetype));
Enter fullscreen mode Exit fullscreen mode

Possibly true on an older build, but following that advice today won't fix Office uploads, because the cause is elsewhere.

The limitation to know before you design around this

Fixing the allowlist doesn't get you fine-grained control.

  • .doc and .xls are both application/x-cfb (measured). Indistinguishable. .ppt shares the OLE2 container so it should behave the same, though I didn't verify that one.
  • .docx and .xlsx are both application/zip — as is any file whose contents are a ZIP archive.

So allowing application/x-cfb admits every legacy Office format, and allowing application/zip admits ZIP files generally. "Accept Word documents only" is not expressible as a MIME rule here.

Seen that way, the forum reporter's decision to allow all formats wasn't as careless as it first looks. Once Office files are in scope, MIME restrictions are a coarse instrument. If you need real enforcement, validate after upload — in a workflow or custom handler that checks both the extension and the contents.

Bonus: when the size limit isn't yours

A contemporaneous report (t/13551) describes uploads failing above 1MB despite the file manager being set to 20GB. Official staff diagnosed a reverse proxy returning 413 and pointed at client_max_body_size.

That's correct, and worth pinning down precisely: the official Docker image does bundle nginx, but ships it with client_max_body_size 0 — unlimited.

# /etc/nginx/conf.d/nocobase.conf inside the container
client_max_body_size 0;
Enter fullscreen mode Exit fullscreen mode

A 3MB upload goes through on a stock image, which I confirmed. If you're capped at 1MB, the cap belongs to a reverse proxy you put in front (nginx defaults to 1MB). No amount of clicking in the admin UI will move it.

One detail for anyone editing that file: it's a symlink to storage/nocobase.conf, and storage is the volume-backed path — so your edits survive recreating the container.

Takeaways

  • MIME restrictions match against the type detected from file contents, not the declared Content-Type
  • Legacy Office is application/x-cfb, modern Office is application/zip — listing application/msword accomplishes nothing
  • Put the detected values in the allowlist, and accept that this granularity can't express "Word only"
  • Files whose contents can't be identified (text formats) fall back to the extension — a two-tier rule to keep in mind
  • The comma-and-space theory is a dead end on current versions
  • A 1MB ceiling comes from your own reverse proxy, not from NocoBase

Looking at a settings field that asks for MIME types, anyone would assume it gets compared against what the browser sends. It doesn't — and until you know that, the only visible move is to keep rewriting a list that was correct all along. Which is exactly what the forum thread shows.

(Measured on 2.1.23 / PostgreSQL 16. Behavior may change in future versions.)

References

Top comments (0)