Random string with special characters
Letters, digits and a symbol set you can see. The default 74-character alphabet carries 6.2095 bits per character.
More options
Which symbols, and why that is a decision rather than a checkbox
Most generators offer a symbols toggle without saying what it turns on. That makes the entropy figure unverifiable and the compatibility unknowable, and it is the reason a generated password sometimes fails a signup form for no visible reason. Every preset here names its exact set.
The default is twelve characters: exclamation mark, hash, dollar, percent, asterisk, plus, hyphen, equals, question mark, at, caret and underscore. Each of those appears in the Microsoft Entra ID allow-list, the Windows Active Directory Passfilt category set and the OWASP Authentication Cheat Sheet list, so a password built from this set will be accepted wherever those rules apply. Added to the 62 alphanumerics it gives an alphabet of 74 and 6.209453 bits per character, which is only 5.3% below the 94-character printable ASCII set.
The question mark is the one exception worth knowing about. It is not in the AWS IAM non-alphanumeric list, so an AWS-specific preset of eleven symbols ships separately at 73 characters and 6.189825 bits rather than weakening the default for everyone else.
| Preset | Symbols | Alphabet size | Bits per character | Use it for |
|---|---|---|---|---|
| Safe symbols (default) | ! # $ % * + - = ? @ ^ _ | 74 | 6.209453 | Anything that has to pass a corporate password policy |
| Maximum compatibility | ! # $ % * + - | 69 | 6.108524 | The intersection of the strictest published policies |
| AWS IAM | The safe set without ? | 73 | 6.189825 | IAM user passwords |
| All OWASP symbols | All 32 punctuation marks plus space | 95 | 6.569856 | A value going straight into a password manager |
| URL safe | - _ | 64 | 6.000000 | API keys and bearer tokens |
What breaks where
A symbol is only a problem in a context that assigns it a meaning. Four contexts account for almost every real failure, and in three of them the fix is quoting rather than a different alphabet.
In a POSIX shell, an unquoted argument exposes dollar, backtick, backslash, asterisk, question mark, square brackets, hash, tilde, semicolon, ampersand, pipe and the redirection characters to expansion. Wrapping the value in single quotes removes every one of those meanings, because the only character that is special inside single quotes is the single quote itself, which is not in any preset here. Double quotes are not sufficient: dollar, backtick and backslash stay live inside them, and in an interactive bash session so does the exclamation mark, through history expansion.
In a URL, RFC 3986 makes only letters, digits, hyphen, period, underscore and tilde unreserved. Everything else has to be percent-encoded. Two failures are specific rather than general: a plus in an application/x-www-form-urlencoded value decodes to a space, and a hash truncates the value at the fragment boundary. Ampersand and equals split query parameters, and question mark starts the query string.
In SQL, the danger is not the symbol but the interpolation. A parameterised query takes any of these values without escaping. Two symbols in the default set still deserve attention after that: percent and underscore are wildcards inside a LIKE pattern, so a stored token containing either will match more rows than expected if it is ever compared with LIKE rather than equals.
The CSV problem, which is ours as much as yours
A CSV field whose first character is an equals sign, a plus, a hyphen or an at sign is interpreted as a formula by Excel and Google Sheets. That is CSV injection, and it is not hypothetical: opening the file is enough to trigger evaluation, and in the worst case a crafted field invokes external data or a shell command through a spreadsheet function.
All four of those characters are in our default symbol set, which means a generated value has a real chance of starting with one. This site exports CSV, so the obligation is ours: every exported field is quoted, and a leading formula character is neutralised. If you build your own export, quote every field or prefix the value with an apostrophe, and do not rely on the value happening not to start with one of the four.
Quoting a CSV field is not by itself sufficient in every spreadsheet import path. The reliable neutraliser is a leading apostrophe, or an import that types the column as text before parsing.
Composition rules are compliance, not security
NIST SP 800-63B-4 is explicit: "Other composition requirements for passwords SHALL NOT be imposed." Its appendix names machine-generated random passwords as a more effective mitigation than composition rules. That is a direct argument for a generator like this one, and it is also a reason to be careful about what the must-include toggle does to the entropy figure.
PCI DSS v4.0.1 requirement 8.3.6 contradicts NIST and requires both numeric and alphabetic characters. Both can be true at once because they are answering different questions, and a tool that has to satisfy an auditor needs to enforce the rule rather than hope. 6.0% of uniform 16-character alphanumeric draws contain no digit at all and would fail that check.
Requiring at least one character from each selected class breaks the independence assumption behind the entropy formula, so the true figure is log2 of the count of valid strings, not length times log2 of alphabet size. The correct implementation is to generate a whole string, test it, and discard and regenerate on failure, which is provably uniform over the valid set. The common shortcut of placing one character per class and shuffling is not uniform over the valid strings even when the shuffle is perfect: in an exhaustive check over a small alphabet, some valid strings came out a third more often than others. At 94 characters and length 8 the expected number of attempts under whole-string rejection is 2.17, so correctness here is cheap.
What twenty characters buys
Twenty characters from the 74-symbol default is 124.19 bits. Twenty-one reaches 130.40 and crosses the 128-bit line where brute force stops being the interesting attack. For comparison, twenty characters of plain alphanumeric is 119.08 bits, so the twelve symbols are worth about one character of length at this size.
That framing matters more than the raw number, because the hash function dominates the outcome. Against MD5 on one current GPU, 220.6 billion guesses per second, a 64-bit secret falls in 1.3 years. Against bcrypt at cost 5, measured at 304,800 hashes per second on the same hardware, the same secret takes 959,000 years. Seven orders of magnitude separate those two, so the entropy of the string is only half of the answer.
Doing this in your own code
JavaScript
// Uniform over the alphabet, with no modulo bias.
// The alphabet is spread into code points first, so an emoji or an
// accented character counts as one symbol rather than two UTF-16 units.
function randomString(length, alphabetStr) {
const alphabet = [...new Set([...alphabetStr])];
const n = alphabet.length;
if (n < 1) throw new RangeError("empty alphabet");
const limit = 4294967296 - (4294967296 % n); // largest multiple of n <= 2^32
const buf = new Uint32Array(1);
let out = "";
for (let i = 0; i < length; i++) {
let x;
do { crypto.getRandomValues(buf); x = buf[0]; } while (x >= limit);
out += alphabet[x % n];
}
return out;
}
randomString(16, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"); The do/while is the whole point. Dropping it in favour of a bare x % n reintroduces modulo bias.
Python 3.6+
import secrets
ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
"".join(secrets.choice(ALPHABET) for _ in range(16))
# secrets.choice is backed by secrets.randbelow, which rejects and redraws,
# so it is uniform. random.choice is not a secure source and must not be used. Iterating a Python str yields code points, so non-ASCII alphabets work without extra handling.
Go
import (
"crypto/rand"
"math/big"
)
func RandomString(length int, alphabet string) (string, error) {
runes := []rune(alphabet)
n := big.NewInt(int64(len(runes)))
out := make([]rune, length)
for i := range out {
j, err := rand.Int(rand.Reader, n) // rejection sampled internally
if err != nil {
return "", err
}
out[i] = runes[j.Int64()]
}
return string(out), nil
} Convert to []rune, not []byte. Indexing a Go string yields bytes, which splits any multi-byte character.
Rust
// rand = "0.9"
use rand::Rng;
const ALPHABET: &[u8] =
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
fn random_string(length: usize) -> String {
// rand::rng() is a thread-local ChaCha12 CSPRNG seeded from the OS.
let mut rng = rand::rng();
(0..length)
.map(|_| ALPHABET[rng.random_range(0..ALPHABET.len())] as char)
.collect()
} random_range rejection-samples, so it is unbiased. On rand 0.8 the method is named gen_range and the constructor is rand::thread_rng().
PHP 7+
<?php
function random_string(int $length, string $alphabet): string {
$n = strlen($alphabet);
$out = '';
for ($i = 0; $i < $length; $i++) {
$out .= $alphabet[random_int(0, $n - 1)];
}
return $out;
}
echo random_string(16, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'); random_int throws on entropy failure and is unbiased. rand and mt_rand are neither.
Java 8+
import java.security.SecureRandom;
static String randomString(int length, String alphabet) {
SecureRandom rng = new SecureRandom();
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++) {
sb.append(alphabet.charAt(rng.nextInt(alphabet.length())));
}
return sb.toString();
} nextInt(bound) rejects and redraws, so it is uniform. Reuse one SecureRandom instance rather than constructing one per call.
C# / .NET
using System.Security.Cryptography;
const string Alphabet =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
// .NET 8 and later
string s = RandomNumberGenerator.GetString(Alphabet, 16);
// Earlier versions, same distribution
var sb = new System.Text.StringBuilder(16);
for (int i = 0; i < 16; i++)
sb.Append(Alphabet[RandomNumberGenerator.GetInt32(Alphabet.Length)]); Shell
# 16 alphanumeric characters from the kernel CSPRNG.
LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 16; echo
# tr -dc discards every byte outside the set rather than folding it in,
# which makes it a rejection sampler. Each of the 62 bytes has exactly one
# preimage, so the output is uniform.
# openssl, when you want bytes rather than a character set
openssl rand -hex 16
openssl rand -base64 24 Questions
Which special characters are safe to use in a password?
The twelve in the default set here: ! # $ % * + - = ? @ ^ _. Every one of them appears in the Microsoft Entra ID, Windows AD Passfilt and OWASP allow-lists. The question mark is absent from the AWS IAM non-alphanumeric list, so use the AWS IAM preset if the password is for an IAM user.
Why did my generated password get rejected by a website?
Usually one of three reasons. The site has an undocumented maximum length, which is a sign it is not hashing the password. It bans a symbol you used, most often space, quote or backslash. Or it requires a class you happened not to draw, which is why the must-include toggle exists. Switching to the maximum compatibility preset resolves most of these.
Do symbols actually make a password stronger?
They add 0.255 bits per character over plain alphanumeric, going from 5.954 to 6.209. That is real but small: twenty characters with symbols is 124.19 bits against 119.08 without. Adding one more character is worth slightly more than adding the whole symbol set. Length is the cheaper lever, which is exactly why NIST forbids composition requirements and sets a length floor instead.
Why does my CSV export show a formula instead of the string?
The value starts with an equals, plus, hyphen or at sign, and Excel or Google Sheets read that as a formula. All four are in the default symbol set. Prefix the field with an apostrophe, or import the column as text. Our own CSV export handles this, because the risk is created by the character set we ship.
Is it safe to put a symbol-containing string in a shell command?
Single-quote it and yes, entirely. The only character that retains meaning inside single quotes is the single quote itself, which is not in any preset on this page. Double quotes are not enough: dollar, backtick and backslash stay active inside them.
Does the entropy figure account for the must-include rule?
Yes. Requiring at least one character per class means the characters are no longer independent, so the plain formula overstates the truth. The displayed value is log2 of the number of valid strings, computed by inclusion and exclusion, and the generator uses whole-string rejection so the output is genuinely uniform over that set.