Dismiss
Announcing Stack Overflow Documentation

We started with Q&A. Technical documentation is next, and we need your help.

Whether you're a beginner or an experienced developer, you can contribute.

Sign up and start helping → Learn more about Documentation →

Consider a scenario, where std::string is used to store a secret. Once it is consumed and is no longer needed, it would be good to cleanse it, i.e overwrite the memory that contained it, thus hiding the secret.

std::string provides a function const char* data() returning a pointer to (since C++11) continous memory.

Now, since the memory is continous and the variable will be destroyed right after the cleanse due to scope end, would it be safe to:

char* modifiable = const_cast<char*>(secretString.data());
OpenSSL_cleanse(modifiable, secretString.size());

According to standard quoted here:

$5.2.11/7 - Note: Depending on the type of the object, a write operation through the pointer, lvalue or pointer to data member resulting from a const_cast that casts away a const-qualifier68 may produce undefined behavior (7.1.5.1).

That would advise otherwise, but do the conditions above (continuous, to-be-just-removed) make it safe?

share|improve this question
6  
Instead of using OpenSSL_cleanse and possible UB why not iterate through the string and assign it random values from a PRNG? – NathanOliver 2 days ago
9  
Also note starting in C++17 data is overloaded to return a non const pointer so you can use it then. – NathanOliver 2 days ago
3  
@NathanOliver: I'd assume OpenSSL_cleanse is optimized to use as few rand() (or similiar) calls as possible, while guaranteeing acceptable safety. Imagine a case where rand() is called on hardware, and could be costly. Besides, reinventing the wheel... And another thing - as @ilotXXI stated in comment below - naive clear may be optimized away (usually is). – hauron 2 days ago
11  
Note that there are several other things you need to do properly for secret handling. Among them are using mlock'd memory so it can't be written to disk in swap, making sure it is not crossing a cache line boundary so attackers can't try to detect things about it using hyperthreading and cache line bounce counts. Etc. Really look into just using a proper security library and use its data structures, not std::string. – Zan Lynx 2 days ago
3  
You know, you really don't want to use std::string, but an actual securestring-type to ensure all buffers get scrubbed. Take a look at codereview.stackexchange.com/questions/107991/… for a way to hack std::basic_string to fill the need. – Deduplicator yesterday
up vote 15 down vote accepted

It is probably safe. But not guaranteed.

However, since C++11, a std::string must be implemented as contiguous data so you can safely access its internal array using the address of its first element &secretString[0].

if(!secretString.empty()) // avoid UB
{
    char* modifiable = &secretString[0];
    OpenSSL_cleanse(modifiable, secretString.size());
}
share|improve this answer
1  
I personally like this answer, however, the first two sentences contain too much probability :) If the data ended in a read-only memory segment (or would be however optimized by the compiler to be read-only), could that cause the application to enter the UB area? (as in: is operator& much different than a const_cast) – hauron 2 days ago
6  
@hauron Taking the address of a read-write element must produce a read-write pointer. I don't see how the compiler can mess with that concept tbh. – Galik 2 days ago
4  
How could the data end in a read-only segment when it's allocated from the heap in a contiguous block, and written to (to store the null terminator after the string contents)? You get a char* this way, so you can write to it. The standard says it is undefined to write through the const char* returned by data(). One is obviously safe and one is explicitly undefined. There should be no question which to use. – Jonathan Wakely 2 days ago
1  
@JonathanWakely and Galik: you are both correct. It is a modifiable value and it's a part of a continous block. Safe to modify. – hauron 2 days ago
    
The goal was to cleanse std::string internal buffer avoiding UB - this does it, simply. – hauron 2 days ago

std::string is a poor choice to store secrets. Since strings are copyable and sometimes copies go unnoticed, your secret may "get legs". Furthermore, string expansion techniques may cause multiple copies of fragments (or all of) your secrets.

Experience dictates a movable, non-copyable, wiped clean on destroy, unintelligent (no tricky copies under-the-hood) class.

share|improve this answer
4  
It's the "wiped clean on destroy" subproblem that OP is trying to solve. – Barry 2 days ago
3  
I am asserting that the destructor is the best place for a wipe. The onus is on the class designer and not every instance of its use – David Thomas 2 days ago
1  
@DavidThomas But the compiler may optimize away the reset in the destructor as it knows the object is not used anymore. – NathanOliver 2 days ago
3  
@DavidThomas consider this: SecretHandler(std::string&& unprotected); - how would you scrap the unprotected input? (that's what the question is about) – hauron 2 days ago
3  
@hauron Challenging the assumptions of a question is a perfectly legitimate answer. "The answer can be “don’t do that”, but it should also include 'try this instead'." See How do I write a good answer?. It's the onus of the question asker to specify what restrictions they have to operate under, and in cases where doing something is a bad idea, it's important and necessary to specify why you can't get away from it. – jpmc26 yesterday

The standard explicitly says you must not write to the const char* returned by data(), so don't do that.

There are perfectly safe ways to get a modifiable pointer instead:

if (secretString.size())
  OpenSSL_cleanse(&secretString.front(), secretString.size());

Or if the string might have been shrunk already and you want to ensure its entire capacity is wiped:

if (secretString.capacity()) {
  secretString.resize(secretString.capacity());
  OpenSSL_cleanse(&secretString.front(), secretString.size());
}
share|improve this answer
3  
This, plus ensuring that you have reserved enough space for the whole password before entering it into the string (or enlarging the buffer may leave parts of the password behind), is the answer. Although is even that enough to ensure that there is nowhere else that you're not OpenSSL_cleanseing? – Lightness Races in Orbit yesterday
    
Question about capacity: in the case of a SSO string, could the call to resize move from inline-storage to heap-storage? (thus cleansing the heap-storage when the secret was stored inline) I don't quite see why it would but... – Matthieu M. yesterday
1  
Only if it was written by an idiot. Adding elements while there is spare capacity() should never allocate, it would make reserve() completely pointless, although I don't see such a guarantee in the standard. If you don't trust your implementation to get that right then you definitely shouldn't be using std::string for sensitive data. – Jonathan Wakely yesterday
1  
@MatthieuM: if you're worried about that you can cleanse, resize, cleanse again. It looks to me as if the standard allows resize() to invalidate iterators and references even if the new size is within the capacity, so the standard panders to (as Jonathan puts it) idiots. – Steve Jessop yesterday
2  
@JonathanWakely: Well, I think we all agree you probably shouldn't be using std::string for sensitive data anyway :) – Matthieu M. yesterday

You can use std::fill to fill the string with trash:

std::fill(str.begin(),str.end(), 0);

Do note that simply clearing or shrinking the string (with methods such clear or shrink_to_fit) does not guarantee that the string data will be deleted from the process memory. Malicious processes may dump the process memory and can extract the secret if the string is not overwritten correctly.

Bonus: Interestingly, the ability to trash the string data for security reasons forces some programming languages like Java to return passwords as char[] and not String. In Java, String is immutable, so "trashing" it will make a new copy of the string. Hence, you need a modifiable object like char[] which does not use copy-on-write.

Edit: if your compiler does optimize this call out, you can use specific compiler flags to make sure a trashing function will not be optimized out:

#ifdef WIN32

#pragma optimize("",off)
void trashString(std::string& str){
   std::fill(str.begin(),str.end(),0);
}
#pragma optimize("",on)

#endif

#ifdef __GCC__

void __attribute__((optimize("O0"))) trashString(std::string& str) {
       std::fill(str.begin(),str.end(),0);
}


#endif

#ifdef __clang__

void __attribute__ ((optnone))  trashString(std::string& str) {
       std::fill(str.begin(),str.end(),0);
}

#endif
share|improve this answer
10  
I heard that "casual" ways like std::fill or memset can be optimized by compilers. If compiler sees no following usage of the string, it doesn't produce excess data change. Functions like OpenSSL_cleanse are made to prevent it. Am I right? – ilotXXI 2 days ago
2  
@ilotXXI that is true. OpenSSL_cleanse seems to never be optimized away. – hauron 2 days ago
3  
You may need to give some consideration to guarantees that the standard doesn't provide: Depending how you assembled the string, it's possible that some portion of it is left elsewhere in memory if std::string has to do a relocation, or if copies were made somewhere in the call stack. – kfsone 2 days ago
6  
MSVC created memset_s for the exact reason that memset can be optimized away by todays optimizing compilers. It is getting harder and harder to trash the memory of a no longer used object as the compiler/linker sees it as unneeded. – NathanOliver 2 days ago
5  
Why are you having a hard time believing that the compiler would optimize away code that wouldn't have observable side-effects in a way that the C++ object model understands? – Barry 2 days ago

There's a better answer: don't!

std::string is a class which is designed to be userfriendly and efficient. It was not designed with cryptography in mind, so there are few guarantees written into it to help you out. For example, there's no guarantees that your data hasn't been copied elsewhere. At best, you could hope that a particular compiler's implementation offers you the behavior you want.

If you actually want to treat a secret as a secret, you should handle it using tools which are designed for handling secrets. In fact, you should develop a threat model for what capabilities your attacker has, and choose your tools accordingly.

share|improve this answer
2  
How do you deal with the case where you have to interface with a third party that doesn't understand your SecretKeeper class? – LThode yesterday
    
@LThode And that third party doesn't have an OtherSecretKeeper you can convert to? Why are you using them? – JAB yesterday
    
@JAB -- some third parties aren't so well designed, sadly :/ (and sometimes you don't have a choice in the matter, either) – LThode yesterday
1  
At some point along the discussion of third party libraries and such, a threat model has to come up. When you're talking about randomizing memory, rather than simply zeroing it out, you're after a particularly demanding level of santization. Your third party library may simply not be the best choice given your needs. – Cort Ammon yesterday
2  
@JAB, I'm passing them to another program, via stdout. Or I'm base64 encoding them. Or hashing them. Or something generic that works on std::string data. – Paul Draper yesterday

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.