创建一个可变的java.lang.String

bes*_*sss 50 java security string

众所周知,Java String是不可变的.从一开始,不可变字符串就是java的重要补充.与C风格的字符串相比,不变性允许快速访问和大量优化,显着减少了错误,并有助于实施安全模型.

可以在不使用黑客的情况下创建可变的,即

  • java.lang.reflect
  • sun.misc.Unsafe
  • bootstrap类加载器中的类
  • JNI(或JNA,因为它需要JNI)

但它是否可以只用普通的Java,以便可以随时修改字符串?问题是如何

mha*_*ler 80

java.lang.String使用Charset构造函数创建一个可以注入自己的Charset,它可以自带CharsetDecoder.的CharsetDecoder到达一个基准CharBuffer在decodeLoop方法对象.CharBuffer包装原始String对象的char [].由于CharsetDecoder具有对它的引用,您可以使用CharBuffer更改底层char [],因此您有一个可变的String.

public class MutableStringTest {


    // http://stackoverflow.com/questions/11146255/how-to-create-mutable-java-lang-string#11146288
    @Test
    public void testMutableString() throws Exception {
        final String s = createModifiableString();
        System.out.println(s);
        modify(s);
        System.out.println(s);
    }

    private final AtomicReference<CharBuffer> cbRef = new AtomicReference<CharBuffer>();
    private String createModifiableString() {
        Charset charset = new Charset("foo", null) {
            @Override
            public boolean contains(Charset cs) {
                return false;
            }

            @Override
            public CharsetDecoder newDecoder() {
                CharsetDecoder cd = new CharsetDecoder(this, 1.0f, 1.0f) {
                    @Override
                    protected CoderResult decodeLoop(ByteBuffer in, CharBuffer out) {
                        cbRef.set(out);
                        while(in.remaining()>0) {
                            out.append((char)in.get());
                        }
                        return CoderResult.UNDERFLOW;
                    }
                };
                return cd;
            }

            @Override
            public CharsetEncoder newEncoder() {
                return null;
            }
        };
        return new String("abc".getBytes(), charset);
    }
    private void modify(String s) {
        CharBuffer charBuffer = cbRef.get();
        charBuffer.position(0);
        charBuffer.put("xyz");
    }

}
Run Code Online (Sandbox Code Playgroud)

运行代码打印

abc
zzz
Run Code Online (Sandbox Code Playgroud)

我不知道如何正确实现decodeLoop(),但我现在不在乎:)

  • 好消息是,如果将`System.getSecurityManager()`作为返回的`char []`复制,则不存在安全漏洞. (7认同)

Mec*_*ail 9

这个问题得到了@mhaller的一个很好的答案.我会说所谓的拼图很简单,只要看看String的可用c-tors就应该能够找出它部分,

演练

感兴趣的C-tor是下面的,如果你要闯入/破解/寻找安全漏洞总是寻找非最终的任意类.这里的案例是java.nio.charset.Charset


//String
public String(byte bytes[], int offset, int length, Charset charset) {
    if (charset == null)
        throw new NullPointerException("charset");
    checkBounds(bytes, offset, length);
    char[] v = StringCoding.decode(charset, bytes, offset, length);
    this.offset = 0;
    this.count = v.length;
    this.value = v;
}
c-tor byte[]通过传递Charset而不是chartset名称提供了所谓的快速转换为String的方法,以避免查找chartsetName-> charset.它还允许传递任意Charset对象来创建String.字符集主路由的内容转换java.nio.ByteBufferCharBuffer.CharBuffer可以保存对char []的引用,并且它可以通过array(),CharBuffer也是完全可修改的.


    //StringCoding
    static char[] decode(Charset cs, byte[] ba, int off, int len) {
        StringDecoder sd = new StringDecoder(cs, cs.name());
        byte[] b = Arrays.copyOf(ba, ba.length);
        return sd.decode(b, off, len);
    }

    //StringDecoder
    char[] decode(byte[] ba, int off, int len) {
        int en = scale(len, cd.maxCharsPerByte());
        char[] ca = new char[en];
        if (len == 0)
            return ca;
        cd.reset();
        ByteBuffer bb = ByteBuffer.wrap(ba, off, len);
        CharBuffer cb = CharBuffer.wrap(ca);
        try {
            CoderResult cr = cd.decode(bb, cb, true);
            if (!cr.isUnderflow())
                cr.throwException();
            cr = cd.flush(cb);
            if (!cr.isUnderflow())
                cr.throwException();
        } catch (CharacterCodingException x) {
            // Substitution is always enabled,
            // so this shouldn't happen
            throw new Error(x);
        }
        return safeTrim(ca, cb.position(), cs);
    }
Run Code Online (Sandbox Code Playgroud)

为了防止改变,char[]java开发人员像任何其他String构造一样复制数组(例如public String(char value[])).但是有一个例外 - 如果没有安装SecurityManager,则不会复制char [].

    //Trim the given char array to the given length
    //
    private static char[] safeTrim(char[] ca, int len, Charset cs) {
        if (len == ca.length 
                && (System.getSecurityManager() == null
                || cs.getClass().getClassLoader0() == null))
            return ca;
        else
            return Arrays.copyOf(ca, len);
    }
Run Code Online (Sandbox Code Playgroud)

So if there is no SecurityManager it's absolutely possible to have a modifiable CharBuffer/char[] that's being referenced by a String.

Everything looks fine by now - except the byte[]也被复制(上面的粗体).这是Java开发人员懒惰和严重错误的地方.

该副本是必要的,以防止流氓Charset(上面的例子)能够改变源字节[].但是,假设有大约512KB byte[]缓冲区包含少量String的情况.试图创建一个小的,少数几个图表 - new String(buf, position, position+32,charset) 导致大量512KB byte []复制.如果缓冲区大约是1KB,那么影响将永远不会被发现.但是,对于大缓冲区,性能影响非常大.简单的解决方法是复制相关部分.

......或者java.nio通过引入只读缓冲区的思想设计者.简单地调用ByteBuffer.asReadOnlyBuffer()就足够了(如果Charset.getClassLoader()!= null)*有时即使是那些工作的人java.lang也会完全错误.

*Class.getClassLoader()为引导类返回null,即与JVM本身一起发布的类.


kei*_*iki 5

我会说StringBuilder(或StringBuffer用于多线程使用).是的,最后你得到一个不可变的字符串.但这是要走的路.

例如,在循环中附加字符串的最佳方法是使用StringBuilder.当您使用"fu"+ variable +"ba"时,Java本身使用StringBuilder.

http://docs.oracle.com/javase/6/docs/api/java/lang/StringBuilder.html

追加(泡壳).append(5).appen( "dfgdfg")的toString();

  • 没有字符串是**最终**类.CharSequence是**接口**.在simalar的基础上,两者都扩展(间接用于StringBiuilder/Buffer)java.lang.Object.问题是关于`java.lang.String`. (2认同)