In fiddling around with HTML5 desktop sourced drag and drop, present in Safari Version 4.0.3 (6531.9), I’m faced with the interesting challenge of understanding when I can trust that a drop is really a drop – that a File is the result of user interaction. For a little context, here’s a bit of code cobbled up by Gordon Durand that’ll let us capture desktop sourced drops in the latest snow leopard:
<html> <head> <script> function dodragenter(event) { document.getElementById("output").textContent = "Drop it! I dare you!"; } function dodragleave(event) { document.getElementById("output").textContent = ""; } function dodrop(event) { var files = event.dataTransfer.files; var uris = event.dataTransfer.getData("text/uri-list").split("\n"); var msg; msg = "File Count: " + files.length + "\n"; for (var i = 0; i < files.length; i++) { msg += (" File " + i + ":\n"); msg += ("\tfileData.fileName: " + files[i].fileName + ", fileData.fileSize: " + files[i].fileSize + "\n"); msg += ("\turi: " + uris[i] + "\n"); msg += "\n"; } document.getElementById("output").textContent = msg; } </script> </head> <body> <div id="output" style="min-height: 100px; white-space: pre; border: 1px solid black;" ondragenter="event.stopPropagation(); event.preventDefault(); dodragenter(event);" ondragover="event.stopPropagation(); event.preventDefault();" ondrop="event.stopPropagation(); event.preventDefault(); dodrop(event);" ondragleave="dodragleave(event)"> </body> </html>
What’s a file look like?
So the first question is, in what ways can we introspect a javascript object? Here’s an interesting start:
// examine a supposed File object and return its vitals function getObjectInfo(o) { var msg; msg = "typeof(o): " + typeof o + "\n"; msg += "o.toString(): " + o.toString() + "\n"; msg += "String(o): " + String(o) + "\n"; if (o) msg += "o.constructor: " + o.constructor + "\n"; if (o) msg += "o.prototype: " + o.prototype + "\n"; if (o) msg += "o members: "; var comma = ""; for (var m in o) { msg += comma + m; comma = ", "; } msg += "\n\ncontents: (" + o.fileName + " is " + o.fileSize + " bytes)\n"; return "<pre>"+msg+"</pre>>"; }
That is, we’re checking .constructor
, .prototype
and the members
of this magical file object. Here’s what a “real” file object looks
like, one caught by a drag:
typeof(o): object o.toString(): [object File] String(o): [object File] o.constructor: [object FileConstructor] o.prototype: undefined o members: fileSize, fileName contents: (silly_hat.jpg is 1380 bytes)
Hrm, can we fake that? Here’s a first try:
var fakeFile = { fileSize: 1234, fileName: "/etc/passwd" }; fakeFile.constructor = File; fakeFile.toString = function() { return "[object File]"; }
the fakeFile
variable ends up looking like this:
typeof(o): object o.toString(): [object File] String(o): [object File] o.constructor: [object FileConstructor] o.prototype: undefined o members: fileSize, fileName, constructor, toString contents: (/etc/passwd is 1234 bytes)
The only telltale here is the fact that I’ve overridden constructor and the toString functions to make a plain ol' object instance built from a literal look a lil' bit more like one of these mystical files. So I, personally, can’t figure out how to get rid of this constructor or toString function. So I’m concluding here that we’ve got a reasonable way to filter out wholly synthetic fake File objects.
Now what about a different tactic? overwriting an existing File
object? We’ll take something read (the first object), and overwrite
the fileSize
and fileName
members. And that doesn’t work. we see
that the specification of fileName and fileSize is actually read only.
Here’s the idl from WebKit –
module html { interface [ GenerateConstructor ] File { readonly attribute DOMString fileName; readonly attribute unsigned long long fileSize; }; }
Testing for an authentic File
This is deeply tied to Safari 4.0.3, and subject to break in the future, but for now this is the (slightly redundant) test that I come up with to verify that we’re really talking about a File that was attained as a result of a drag.
function isRealFile(f) { var hasFileName = false, hasFileSize = false, numMembers = 0, mutableMembers = false; for (var m in f) { numMembers++; if (m === 'fileName') hasFileName = true; else if (m === 'fileSize') hasFileSize = true; var before = f[m]; f[m] = "__IMutedYou__"; if (before !== f[m]) mutableMembers = true; } if (typeof(f) !== 'object' || !f.toString || f.toString.constructor !== Function || f.toString() !== '[object File]' || f.constructor != File || f.protype != undefined || numMembers != 2 || !hasFileName || !hasFileSize || mutableMembers) { return false; } return true; }
Cobbling together an Attack?
The ultimate question is once we have one of these magic File objects (resulting from a user drop), what can we do with it that’s interesting? In 15 minutes of looking it seems like all we can really do is display the size, uri, and filename… Could that be correct?
The point here is that it’s very interesting that we can now accept drops from the desktop, but what will we be able to do with them and what will the security model be?
Personally, I’m grateful to see progress in this area, but am a little curious about the utility of these first steps, and the strategy for the next ones…
lloyd