How to check which file types a file input accepts

Read input.accept in the Console and split it into extensions and MIME types. Then attach a file that matches neither and read input.files[0] and input.validity.valid. On the form below, a .txt file attached cleanly, the input stayed valid, and the endpoint stored it.

Why check this

Run this when a file input is added or its accept list changes, and again before any release that claims the upload is restricted. The attribute reads like a rule and behaves like a suggestion.

The failure it prevents is an endpoint that stores whatever arrives because the ticket said the input only takes images. accept filters what the operating system picker offers. It does not bar a file from the control, it raises no validation error, and it has no effect at all on a request built by a script or by curl. The type check has to exist on the server, and the only way to know whether it does is to send a file that breaks the rule.

Prerequisites

node -e "const fs=require('fs');
const png=Buffer.from('89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000a49444154789c6360000002000100ffff03000006000557bfabd40000000049454e44ae426082','hex');
fs.writeFileSync('shot.png',png);
fs.writeFileSync('report.pdf','%PDF-1.4\n1 0 obj<</Type/Catalog>>endobj\n%%EOF\n');
fs.writeFileSync('notes.txt','plain text, not an image and not a pdf\n');
fs.writeFileSync('invoice.pdf',png);
fs.writeFileSync('logo.png','%PDF-1.4\nthis is a pdf wearing a png name\n%%EOF\n');
fs.writeFileSync('archive',Buffer.from('504b0304140000000800','hex'));"

Steps

  1. Step 1.

    Open the page and read what the input declares.

    const el = document.getElementById('doc'), t = el.accept.split(',').map((x) => x.trim());
    `accept="${el.accept}"
    extensions: ${t.filter((x) => x.startsWith('.')).join(' ')}
    mime types: ${t.filter((x) => !x.startsWith('.')).join(' ')}
    required=${el.required} multiple=${el.multiple} files.length=${el.files.length}`;
    
    accept="image/png,image/jpeg,.pdf"
    extensions: .pdf
    mime types: image/png image/jpeg
    required=true multiple=false files.length=0

    Two kinds of token in one list. .pdf matches on the file name, image/png matches on the type the browser assigned. They are compared differently, and a file can match one and fail the other.

  2. Step 2.

    Attach each fixture in turn and read what the control reports. Use elementHandle.uploadFile() in your automation, which is the same call the DevTools protocol exposes.

    const el = document.getElementById('doc'), file = el.files[0];
    const tokens = el.accept.split(',').map((x) => x.trim().toLowerCase());
    const byExt = tokens.some((t) => t.startsWith('.') && file.name.toLowerCase().endsWith(t));
    const byMime = tokens.some((t) => !t.startsWith('.') && (t === file.type || (t.endsWith('/*') && file.type.startsWith(t.slice(0, -1)))));
    `name=${file.name} size=${file.size} type="${file.type}"
    matches accept by extension=${byExt}  by mime=${byMime}
    input.validity.valid=${el.validity.valid} validationMessage="${el.validationMessage}" form.checkValidity()=${document.getElementById('docs').checkValidity()}`;
    
    name=shot.png size=75 type="image/png"
    matches accept by extension=false  by mime=true
    input.validity.valid=true validationMessage="" form.checkValidity()=true
    
    name=notes.txt size=39 type="text/plain"
    matches accept by extension=false  by mime=false
    input.validity.valid=true validationMessage="" form.checkValidity()=true
    
    name=invoice.pdf size=75 type="application/pdf"
    matches accept by extension=true  by mime=false
    input.validity.valid=true validationMessage="" form.checkValidity()=true
    
    name=logo.png size=48 type="image/png"
    matches accept by extension=false  by mime=true
    input.validity.valid=true validationMessage="" form.checkValidity()=true
    
    name=archive size=10 type=""
    matches accept by extension=false  by mime=false
    input.validity.valid=true validationMessage="" form.checkValidity()=true

    input.validity.valid=true on every file, notes.txt and archive included. Neither matches a single token in accept, and constraint validation does not care. The file with no extension got an empty type, so a MIME comparison on that row compares against nothing.

  3. Step 3.

    Read the first bytes of the two files whose names lie about their contents.

    const f = document.getElementById('doc').files[0];
    const b = new Uint8Array(await f.slice(0, 8).arrayBuffer());
    `${f.name}: declared type "${f.type}", first bytes ${[...b].map((x) => x.toString(16).padStart(2, '0')).join(' ')}`;
    
    invoice.pdf: declared type "application/pdf", first bytes 89 50 4e 47 0d 0a 1a 0a
    logo.png: declared type "image/png", first bytes 25 50 44 46 2d 31 2e 34

    89 50 4e 47 is the PNG signature and 25 50 44 46 is %PDF. Both files are the opposite of their names, and file.type followed the extension in both cases. The browser did not open either file.

  4. Step 4.

    Submit the disallowed .txt through the form itself, with no script bypass.

    document.getElementById('docs').querySelector('button').click();
    
    {"stored":true,"filename":"notes.txt","fileBytes":39,"requestBytes":222,"truncated":false}

    The form navigated and the server answered 200. Nothing in the browser stood in the way.

  5. Step 5.

    Post the same file straight to the endpoint, naming a type that appears nowhere in accept.

    curl -s -i -F 'file=@notes.txt;type=text/plain' http://localhost:9317/upload
    
    HTTP/1.1 200 OK
    content-type: application/json
    …
    {"stored":true,"filename":"notes.txt","fileBytes":39,"requestBytes":238,"truncated":false}

    The type in a multipart part is a string the client chose. Treat it as user input.

  6. Step 6.

    Check the file the way the server has to: by its contents.

    head -c 8 invoice.pdf | od -An -tx1
    
     89 50 4e 47 0d 0a 1a 0a

    A file called invoice.pdf, accepted by the .pdf token, holding PNG bytes. If your endpoint keys anything off the extension or off the declared type, this is the file that proves it.

How to read the result

| What you see | What it means | What to do | | --- | --- | --- | | valid=true for a file outside accept | Working as specified. accept is not a constraint | Look for the check on the server, and file a defect if there is none. | | file.type is an empty string | The extension is unknown to the browser | Any MIME comparison silently passes or fails. Match on bytes. | | file.type disagrees with the first bytes | The type came from the extension, not the contents | Send that file to the endpoint and read what it stored. | | The endpoint answers 200 to a disallowed type | The upload has no type check | Raise it against the endpoint. The attribute cannot fix it. |

Common mistakes

Sign: A test attaches a disallowed file, expects an error, and gets a green run instead.Cause: The accept attribute filters the operating system picker and takes no part in constraint validation. In the capture, notes.txt and a file with no extension both gave input.validity.valid=true and form.checkValidity()=true. There is no validity flag for a wrong file type, so there is nothing for the test to assert on in the browser.
Sign: accept="image/png" is treated as the same rule as accept=".png".Cause: They match on different things and they disagree. invoice.pdf matched the .pdf token by name while its MIME type application/pdf matched none of the MIME tokens; shot.png matched image/png while matching no extension token. One list, two comparison rules. A file passes the input if it matches any single token.
Sign: The value of file.type is used as proof of what the file is.Cause: Chrome derives that string from the extension, not from the contents. logo.png reported image/png with first bytes 25 50 44 46, which is %PDF, and invoice.pdf reported application/pdf with the PNG signature 89 50 4e 47. Rename any file and the reported type follows the new name.
Sign: Only the picker path is tested.Cause: Files reach an input through drag and drop, through DataTransfer in a script, and through the DevTools protocol that every automation framework uses. None of those routes consult accept. A request assembled by curl never sees the input element at all, which is step 5.

What to check next

FAQ

Does the accept attribute validate the file type?

No. It sets the default filter in the file picker. The HTML specification gives it no role in constraint validation, and the capture above confirms it: every disallowed file left validity.valid at true and reached the server.

What is the difference between an extension token and a MIME token?

An extension token such as .pdf is compared against the end of the file name. A MIME token such as image/png is compared against file.type, which Chrome derives from that same extension. image/* matches any type in the group.

How do I check a file MIME type before upload?

Read the first bytes with file.slice(0, 8).arrayBuffer() and compare them against the signature for the format. file.type is not evidence, as step 3 shows.

Can a test rely on the picker refusing a file?

No, and the picker cannot be driven from an automated test in the first place. Attach the file programmatically and assert on the response from the endpoint.

What should the server do with a mismatch?

Answer a 4xx with a body naming the field and the accepted types, and store nothing. Silent acceptance and a 500 are both defects.

Verified

Verified by Maks VernyChrome 152.0.7977.76curl 8.21.0Node 22.23.2

Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.

intermediate10 minpublished updated Maks Verny