├── .gitignore ├── LICENSE ├── README.MD ├── javaser.js └── test.html /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | target/ 26 | pom.xml.tag 27 | pom.xml.releaseBackup 28 | pom.xml.versionsBackup 29 | pom.xml.next 30 | release.properties 31 | dependency-reduced-pom.xml 32 | buildNumber.properties 33 | .mvn/timing.properties 34 | 35 | # Avoid ignoring Maven wrapper jar file (.jar files are usually ignored) 36 | !/.mvn/wrapper/maven-wrapper.jar 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Moritz Bechler 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # A Java serializer in JavaScript 2 | 3 | Implementation of native Java serialization in JavaScript. 4 | Also includes two deserialization payload generators 5 | (as seen on [ysoserial](https://github.com/frohoff/ysoserial): 6 | JRMPClient and a JNDI variant of CommonBeanutils1) 7 | as well as a PoC for CVE-2018-2800: 8 | 9 | Up to the April 2018 CPU (6u191, 7u181, 8u171) Java's RMI endpoints 10 | allowed HTTP tunneling of requests. Failing to implement further 11 | restrictions on these requests it was possible to perform them 12 | as cross-origin requests from third-party websites. 13 | This makes it possible to exploit otherwise unreachable RMI endpoints. 14 | 15 | [Blog post with some more info](https://mbechler.github.io/2018/05/21/Java-CVE-2018-2800/) 16 | 17 | ## Disclaimer 18 | 19 | All information and code is provided solely for educational purposes 20 | and/or testing your own systems for these vulnerabilities. 21 | 22 | 23 | ## Notes 24 | 25 | Some browsers/browser plugins may implement further restrictions 26 | trying to disallow requests to local networks. 27 | 28 | The JMX/RMI PoC vectors have already been addressed in an earlier Java release. 29 | -------------------------------------------------------------------------------- /javaser.js: -------------------------------------------------------------------------------- 1 | // primitive types 2 | // * B byte 3 | // * C char 4 | // * D double 5 | // * F float 6 | // * I int 7 | // * J long 8 | // * L class or interface 9 | // * S short 10 | // * Z boolean 11 | // * [ array 12 | 13 | function Null() { 14 | } 15 | 16 | function Boolean(val) { 17 | this.isTrue = val == true; 18 | this.typeCode = 'Z'; 19 | this.primitive = true; 20 | 21 | this.write = function(out) { 22 | if (val == true) { 23 | out.writeByte(1) 24 | } else { 25 | out.writeByte(0); 26 | } 27 | } 28 | } 29 | 30 | function Byte(val) { 31 | this.val = val; 32 | this.typeCode = 'B'; 33 | this.primitive = true; 34 | 35 | this.write = function(out) { 36 | out.writeByte(val); 37 | } 38 | } 39 | 40 | function Short(val) { 41 | this.val = val; 42 | this.typeCode = 'S'; 43 | this.primitive = true; 44 | 45 | this.write = function(out) { 46 | out.writeByte((val >> 8) & 0xFF); 47 | out.writeByte(val & 0xFF); 48 | } 49 | } 50 | 51 | function Char(val) { 52 | this.val = val; 53 | this.typeCode = 'C'; 54 | this.primitive = true; 55 | 56 | this.write = function(out) { 57 | out.writeByte((val >> 8) & 0xFF); 58 | out.writeByte(val & 0xFF); 59 | } 60 | } 61 | 62 | function Integer(val) { 63 | this.val = val; 64 | this.typeCode = 'I'; 65 | this.primitive = true; 66 | 67 | this.write = function(out) { 68 | out.writeByte((val >> 24) & 0xFF); 69 | out.writeByte((val >> 16) & 0xFF); 70 | out.writeByte((val >> 8) & 0xFF); 71 | out.writeByte(val & 0xFF); 72 | } 73 | } 74 | 75 | function Long(high, low) { 76 | this.high = high; 77 | this.low = low; 78 | this.typeCode = 'J'; 79 | this.primitive = true; 80 | 81 | this.write = function(out) { 82 | out.writeByte((high >> 24) & 0xFF); 83 | out.writeByte((high >> 16) & 0xFF); 84 | out.writeByte((high >> 8) & 0xFF); 85 | out.writeByte(high & 0xFF); 86 | out.writeByte((low >> 24) & 0xFF); 87 | out.writeByte((low >> 16) & 0xFF); 88 | out.writeByte((low >> 8) & 0xFF); 89 | out.writeByte(low & 0xFF); 90 | } 91 | } 92 | 93 | function Float(val) { 94 | this.val = val; 95 | this.typeCode = 'F'; 96 | this.primitive = true; 97 | 98 | this.write = function(out) { 99 | throw "Unimplemented"; 100 | } 101 | } 102 | 103 | function Double(val) { 104 | this.val = val; 105 | this.typeCode = 'D'; 106 | this.primitive = true; 107 | 108 | this.write = function(out) { 109 | throw "Unimplemented"; 110 | } 111 | } 112 | 113 | function Array(type, vals) { 114 | this.type = type; 115 | this.vals = vals; 116 | this.typeCode = '['; 117 | this.primitive = false; 118 | 119 | this.write = function(out) { 120 | new Integer(vals.length).write(out); 121 | for (var i = 0; i < vals.length; i++) { 122 | vals[i].write(out); 123 | } 124 | } 125 | } 126 | 127 | function String(val) { 128 | this.val = val; 129 | this.primitive = false; 130 | 131 | this.writeBytes = function(out) { 132 | for (var i = 0; i < val.length; i++) { 133 | out.writeByte(val.charCodeAt(i) & 0xFF); 134 | } 135 | } 136 | 137 | this.writeChars = function(out) { 138 | for (var i = 0; i < val.length; i++) { 139 | out.writeChar(val.charCodeAt(i) & 0xFFFF); 140 | } 141 | } 142 | 143 | this.writeUTF = function(out) { 144 | var utf8b = this.encodeUTF(); 145 | for (var i = 0; i < utf8b.length; i++) { 146 | out.writeByte(utf8b.charCodeAt(i) & 0xFF); 147 | } 148 | } 149 | 150 | this.encodeUTF = function() { 151 | // per 152 | // http://ecmanaut.blogspot.de/2006/07/encoding-decoding-utf8-in-javascript.html 153 | return unescape(encodeURIComponent(val)); 154 | } 155 | } 156 | 157 | function Enum(val) { 158 | this.primitive = false; 159 | } 160 | 161 | function Class(name) { 162 | this.primitive = false; 163 | this.name = name; 164 | } 165 | 166 | function ObjectStreamField(name, val) { 167 | this.name = name; 168 | this.val = val; 169 | this.unshared = false; 170 | } 171 | 172 | function ObjectStreamClass(name, serialHigh, serialLow, fields, opts) { 173 | this.class = new Class(name); 174 | this.superClass = null; 175 | this.primitive = false; 176 | this.proxy = false; 177 | this.enum = false; 178 | this.serialVersionHigh = serialHigh; 179 | this.serialVersionLow = serialLow; 180 | this.fields = fields; 181 | 182 | for ( var key in opts) { 183 | if (opts.hasOwnProperty(key)) { 184 | this[key] = opts[key]; 185 | } 186 | } 187 | 188 | var SC_WRITE_METHOD = 0x01; 189 | var SC_BLOCK_DATA = 0x08; 190 | var SC_SERIALIZABLE = 0x02; 191 | var SC_EXTERNALIZABLE = 0x04; 192 | var SC_ENUM = 0x10; 193 | 194 | this.write = function(out) { 195 | out.writeUTF(name); 196 | new Long(this.serialVersionHigh, this.serialVersionLow).write(out); 197 | var flags = 0; 198 | if ('writeExternal' in this) { 199 | flags |= SC_EXTERNALIZABLE; 200 | if (out.protocol != 1) { 201 | flags |= SC_BLOCK_DATA; 202 | } 203 | } else { 204 | flags |= SC_SERIALIZABLE; 205 | } 206 | 207 | if ('writeObject' in this) { 208 | flags |= SC_WRITE_METHOD; 209 | } 210 | if (this.enum) { 211 | flags |= SC_ENUM; 212 | } 213 | out.writeByte(flags); 214 | 215 | new Short(this.fields.length).write(out); 216 | 217 | for (var i = 0; i < this.fields.length; i++) { 218 | var f = this.fields[i]; 219 | var tc = 0; 220 | if ('typeString' in f) { 221 | tc = f.typeString.charCodeAt(0); 222 | } else { 223 | tc = f.typeCode.charCodeAt(0); 224 | } 225 | out.writeByte(tc); 226 | out.writeUTF(f.name); 227 | if ('typeString' in f) { 228 | out.writeTypeString(f.typeString); 229 | } 230 | } 231 | } 232 | 233 | this.getClassDataLayout = function() { 234 | if ( this.superClass ) { 235 | var r = this.superClass.getClassDataLayout(); 236 | r.push(this); 237 | return r; 238 | } 239 | return [ this ]; 240 | } 241 | 242 | this.writePrimitives = function(out, vals) { 243 | var buf = new DataOutput([]); 244 | for (var i = 0; i < this.fields.length; i++) { 245 | if (!('typeString' in this.fields[i])) { 246 | var v; 247 | if ( this.fields[i].name in vals ) { 248 | v = vals[this.fields[i].name]; 249 | } 250 | else { 251 | var tc = this.fields[i].typeCode; 252 | if ( tc == 'I') { 253 | v = new Integer(0); 254 | } else if ( tc == 'Z' ) { 255 | v = new Boolean(false); 256 | } else if ( tc == 'B' ) { 257 | v = new Byte(0); 258 | } else if ( tc == 'S' ) { 259 | v = new Short(0); 260 | } else if ( tc == 'C' ) { 261 | v = new Char(0); 262 | } else if ( tc == 'L' ) { 263 | v = new Long(0); 264 | } else { 265 | throw "Missing primitive value " + this.fields[i].name; 266 | } 267 | } 268 | v.write(buf); 269 | } 270 | } 271 | out.writeBytes(buf.out); 272 | } 273 | 274 | this.getObjectFieldDescriptors = function() { 275 | var res = []; 276 | for (var i = 0; i < this.fields.length; i++) { 277 | if ('typeString' in this.fields[i]) { 278 | res.push(this.fields[i]); 279 | } 280 | } 281 | return res; 282 | } 283 | } 284 | 285 | function Object(clazz, fieldVals) { 286 | this.clazz = clazz; 287 | this.typeCode = 'L'; 288 | this.values = fieldVals; 289 | } 290 | 291 | function DataOutput(out) { 292 | this.out = out; 293 | this.written = 0; 294 | 295 | this.writeByte = function(b) { 296 | this.out.push(b); 297 | this.written++; 298 | } 299 | 300 | this.writeBytes = function(bs) { 301 | this.out.push.apply(this.out, bs); 302 | this.written += bs.length; 303 | } 304 | } 305 | 306 | function ObjectHandles() { 307 | this.handles = [] 308 | 309 | this.clear = function() { 310 | } 311 | this.assign = function(obj) { 312 | if (obj == null || this.handles.indexOf(obj) < 0) { 313 | return; 314 | } 315 | this.handles.push(obj); 316 | } 317 | 318 | this.lookup = function(obj) { 319 | return this.handles.indexOf(obj); 320 | } 321 | } 322 | 323 | function ObjectOutput(out, opts) { 324 | var STREAM_MAGIC = 0xaced; 325 | var STREAM_VERSION = 5; 326 | var TC_BASE = 0x70; 327 | var TC_NULL = 0x70; 328 | var TC_REFERENCE = 0x71; 329 | var TC_CLASSDESC = 0x72; 330 | var TC_OBJECT = 0x73; 331 | var TC_STRING = 0x74; 332 | var TC_ARRAY = 0x75; 333 | var TC_CLASS = 0x76; 334 | var TC_BLOCKDATA = 0x77; 335 | var TC_ENDBLOCKDATA = 0x78; 336 | var TC_RESET = 0x79; 337 | var TC_BLOCKDATALONG = 0x7A; 338 | var TC_EXCEPTION = 0x7B; 339 | var TC_LONGSTRING = 0x7C; 340 | var TC_PROXYCLASSDESC = 0x7D; 341 | var TC_ENUM = 0x7E; 342 | var TC_MAX = 0x7E; 343 | var baseWireHandle = 0x7e0000; 344 | 345 | this.blockMode = true; 346 | this.blockBuf = []; 347 | this.blockPos = 0; 348 | this.depth = 0; 349 | this.handles = new ObjectHandles(); 350 | this.protocol = 2; 351 | 352 | for ( var key in opts) { 353 | if (opts.hasOwnProperty(key)) { 354 | this[key] = opts[key]; 355 | } 356 | } 357 | 358 | this.writeHeader = function() { 359 | new Short(STREAM_MAGIC).write(out); 360 | new Short(STREAM_VERSION).write(out); 361 | } 362 | 363 | this.setBlockMode = function(bm) { 364 | var obm = this.blockMode; 365 | if (obm == bm) { 366 | return obm; 367 | } 368 | this.flush(); 369 | this.blockMode = bm; 370 | return obm; 371 | } 372 | 373 | this.flush = function() { 374 | if (this.blockPos == 0) { 375 | return; 376 | } 377 | if (this.blockMode) { 378 | this.writeBlockHeader(this.blockPos); 379 | } 380 | 381 | out.writeBytes(this.blockBuf); 382 | this.blockBuf = []; 383 | this.blockPos = 0; 384 | } 385 | 386 | this.clear = function() { 387 | this.handles.clear(); 388 | } 389 | 390 | this.writeBlockHeader = function(len) { 391 | if (len <= 0xFF) { 392 | out.writeByte(TC_BLOCKDATA); 393 | out.writeByte(len); 394 | } else { 395 | out.writeByte(TC_BLOCKDATALONG); 396 | new Integer().write(out); 397 | } 398 | } 399 | 400 | this.writeByte = function(b) { 401 | if (this.blockMode) { 402 | this.blockBuf.push(b); 403 | this.blockPos += 1; 404 | } else { 405 | out.writeByte(b); 406 | } 407 | } 408 | 409 | this.writeBytes = function(bs) { 410 | if (this.blockMode) { 411 | this.blockBuf += bs; 412 | this.blockPos += bs.length; 413 | } else { 414 | out.writeBytes(bs); 415 | } 416 | } 417 | 418 | this.writeNull = function() { 419 | this.writeByte(TC_NULL); 420 | } 421 | 422 | this.writeHandle = function(handle) { 423 | this.writeByte(TC_REFERENCE); 424 | new Integer(baseWireHandle + handle).write(out); 425 | } 426 | 427 | this.writeClass = function(clazz, unshared) { 428 | this.writeByte(TC_CLASS); 429 | this.writeClassDesc(ObjectStreamClass.lookup(cl, true), false); 430 | this.handles.assign(unshared ? null : cl); 431 | } 432 | 433 | this.writeClassDesc = function(desc, unshared) { 434 | var handle; 435 | if (desc == null) { 436 | this.writeNull(); 437 | } else if (!unshared && (handle = this.handles.lookup(desc)) != -1) { 438 | this.writeHandle(handle); 439 | } else if (desc.proxy) { 440 | this.writeProxyDesc(desc, unshared); 441 | } else { 442 | this.writeNonProxyDesc(desc, unshared); 443 | } 444 | } 445 | 446 | this.writeProxyDesc = function(desc, unshared) { 447 | this.writeByte(TC_PROXYCLASSDESC); 448 | this.handles.assign(unshared ? null : desc); 449 | 450 | var cl = desc.forClass(); 451 | var ifaces = cl.getInterfaces(); 452 | bout.writeInt(ifaces.length); 453 | for (var i = 0; i < ifaces.length; i++) { 454 | this.writeUTF(ifaces[i].getName(), true); 455 | } 456 | 457 | // empty annotation block 458 | this.setBlockMode(true); 459 | if ('annotateProxyClass' in this) { 460 | this.annotateProxyClass(desc.class); 461 | } 462 | this.setBlockMode(false); 463 | out.writeByte(TC_ENDBLOCKDATA); 464 | 465 | this.writeClassDesc(desc.superClass, false); 466 | } 467 | 468 | this.writeNonProxyDesc = function(desc, unshared) { 469 | this.writeByte(TC_CLASSDESC); 470 | this.handles.assign(unshared ? null : desc); 471 | 472 | desc.write(this); 473 | // empty annotation block 474 | this.setBlockMode(true); 475 | if ('annotateClass' in this) { 476 | this.annotateClass(desc.class); 477 | } 478 | this.setBlockMode(false); 479 | out.writeByte(TC_ENDBLOCKDATA); 480 | 481 | this.writeClassDesc(desc.superClass, false); 482 | } 483 | 484 | this.writeString = function(str, unshared) { 485 | this.handles.assign(unshared ? null : str); 486 | var utflen = unescape(encodeURIComponent(str)).length; 487 | if (utflen <= 0xFFFF) { 488 | this.writeByte(TC_STRING); 489 | this.writeUTFLen(str, utflen); 490 | } else { 491 | this.writeByte(TC_LONGSTRING); 492 | this.writeLongUTF(str, utflen); 493 | } 494 | } 495 | 496 | this.writeUTF = function(str) { 497 | this.writeUTFLen(str, unescape(encodeURIComponent(str)).length); 498 | } 499 | 500 | this.writeUTFLen = function(str, len) { 501 | if (len > 0xFFFF) { 502 | throw "Length exceeded"; 503 | } 504 | 505 | new Short(len).write(this); 506 | if (len == str.length) { 507 | new String(str).writeBytes(this); 508 | } else { 509 | throw "Unimplemented (non-ASCII string)"; 510 | } 511 | } 512 | 513 | this.writeLongUTF = function(str, len) { 514 | new Long(len).write(this); 515 | if (len == str.length) { 516 | str.writeBytes(this); 517 | } else { 518 | throw "Unimplemented (non-ASCII string)"; 519 | } 520 | } 521 | 522 | this.writeEnum = function(en, desc, unshared) { 523 | this.writeByte(TC_ENUM); 524 | var sdesc = desc.getSuperDesc(); 525 | writeClassDesc((sdesc.forClass() == Enum.class) ? desc : sdesc, false); 526 | this.handles.assign(unshared ? null : en); 527 | this.writeString(en.name(), false); 528 | } 529 | 530 | this.writeArray = function(array, desc, unshared) { 531 | this.writeByte(TC_ARRAY); 532 | this.writeClassDesc(desc, false); 533 | this.handles.assign(unshared ? null : array); 534 | var ccl = desc.forClass().getComponentType(); 535 | if (ccl.isPrimitive()) { 536 | array.write(this); 537 | } else { 538 | var len = array.length; 539 | new Integer(len).write(this); 540 | for (var i = 0; i < len; i++) { 541 | writeObject0(objs[i], false); 542 | } 543 | } 544 | } 545 | 546 | this.writeObject = function(obj) { 547 | this.writeObject0(obj, false); 548 | } 549 | 550 | this.writeObject0 = function(obj, unshared) { 551 | var obm = this.setBlockMode(false); 552 | this.depth++; 553 | try { 554 | if (obj instanceof Null) { 555 | this.writeNull(); 556 | return; 557 | } else if (!unshared && (h = this.handles.lookup(obj)) != -1) { 558 | this.writeHandle(h); 559 | return; 560 | } else if (obj instanceof Class) { 561 | this.writeClass(obj, unshared); 562 | return; 563 | } else if (obj instanceof ObjectStreamClass) { 564 | this.writeClassDesc(obj, unshared); 565 | return; 566 | } 567 | 568 | // writeReplace 569 | 570 | if (obj instanceof String) { 571 | this.writeString(obj.val, unshared); 572 | } else if (obj instanceof Array) { 573 | this.writeArray(obj, obj.clazz, unshared); 574 | } else if (obj instanceof Enum) { 575 | this.writeEnum(obj, obj.clazz, unshared); 576 | } else { 577 | this.writeOrdinaryObject(obj, obj.clazz, unshared); 578 | } 579 | } finally { 580 | this.depth--; 581 | this.setBlockMode(obm); 582 | } 583 | } 584 | 585 | this.writeOrdinaryObject = function(obj, desc, unshared) { 586 | this.writeByte(TC_OBJECT); 587 | this.writeClassDesc(desc, false); 588 | this.handles.assign(unshared ? null : obj); 589 | if ('writeExternal' in desc && !desc.proxy) { 590 | this.writeExternalData(obj, desc); 591 | } else { 592 | this.writeSerialData(obj, desc); 593 | } 594 | } 595 | 596 | this.writeSerialData = function(obj, desc) { 597 | var slots = desc.getClassDataLayout(); 598 | for (var i = 0; i < slots.length; i++) { 599 | var slotDesc = slots[i]; 600 | if ('writeObject' in slotDesc) { 601 | this.setBlockMode(true); 602 | slotDesc.writeObject(this, obj, desc); 603 | this.setBlockMode(false); 604 | out.writeByte(TC_ENDBLOCKDATA); 605 | } else { 606 | this.defaultWriteFields(obj, slotDesc); 607 | } 608 | } 609 | } 610 | 611 | this.writeExternalData = function(obj, desc) { 612 | if (this.protocol == 1) { 613 | desc.writeExternal(this, obj); 614 | } else { 615 | this.setBlockMode(true); 616 | this.setBlockMode(false); 617 | out.writeByte(TC_ENDBLOCKDATA); 618 | } 619 | } 620 | 621 | this.writeFatalException = function(ex) { 622 | clear(); 623 | var oldMode = this.setBlockMode(false); 624 | try { 625 | this.writeByte(TC_EXCEPTION); 626 | this.writeObject0(ex, false); 627 | clear(); 628 | } finally { 629 | this.setBlockMode(oldMode); 630 | } 631 | } 632 | 633 | this.defaultWriteObject = function(obj, desc) { 634 | this.setBlockMode(false); 635 | this.defaultWriteFields(obj, desc); 636 | this.setBlockMode(true); 637 | } 638 | 639 | this.defaultWriteFields = function(obj, desc) { 640 | var cl = desc.clazz; 641 | if (cl != null && obj != null && !cl.isInstance(obj)) { 642 | throw new ClassCastException(); 643 | } 644 | 645 | 646 | desc.writePrimitives(this,obj.values); 647 | var objVals = desc.getObjectFieldDescriptors(obj); 648 | for (var i = 0; i < objVals.length; i++) { 649 | var fdesc = objVals[i]; 650 | if ( fdesc.name in obj.values ) { 651 | this.writeObject0(obj.values[fdesc.name], fdesc.unshared); 652 | } else { 653 | this.writeObject0(new Null(), fdesc.unshared); 654 | } 655 | } 656 | } 657 | 658 | this.writeTypeString = function(type) { 659 | if (type == null) { 660 | this.writeNull(); 661 | } else if ((handle = this.handles.lookup(type)) != -1) { 662 | this.writeHandle(handle); 663 | } else { 664 | this.writeString(type, false); 665 | } 666 | } 667 | } 668 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 53 | 54 | 55 | 56 | 525 | 622 | 623 | 624 | --------------------------------------------------------------------------------