66import net .tokenprotector .config .Config ;
77import net .tokenprotector .monitor .SessionAccessMonitor ;
88import net .tokenprotector .util .Log ;
9+ import net .tokenprotector .util .MinecraftCompat ;
910import org .slf4j .Logger ;
1011import org .slf4j .LoggerFactory ;
1112
@@ -21,7 +22,7 @@ public void onInitializeClient() {
2122
2223 ClientTickEvents .END_CLIENT_TICK .register (client -> {
2324 AlertManager .flushIfNeeded ();
24- if (!osScanDone && client .getToastManager () != null ) {
25+ if (!osScanDone && MinecraftCompat .getToastManager (client ) != null ) {
2526 osScanDone = true ;
2627 scanForOsLeaks ();
2728 }
@@ -36,15 +37,17 @@ private static void scanForOsLeaks() {
3637 if (cfg .monitorEnvironmentVariables ) {
3738 try {
3839 for (var entry : System .getenv ().entrySet ()) {
39- String key = entry .getKey ().toUpperCase ();
40- if (looksLikeSensitiveKey (key ) || looksLikeTokenValue (entry .getValue ())) {
41- String val = entry .getValue ();
40+ String key = entry .getKey ();
41+ String upperKey = key .toUpperCase ();
42+ String val = entry .getValue ();
43+
44+ if (shouldFlagEnvironmentVariable (upperKey , val )) {
4245 boolean isJwt = looksLikeJwt (val );
4346 found .incrementAndGet ();
44- SessionAccessMonitor .recordOsLeak ("env:" + entry . getKey () , isJwt ? "REAL JWT" : "possible token" );
47+ SessionAccessMonitor .recordOsLeak ("env:" + key , isJwt ? "REAL JWT" : "possible token" );
4548 Log .alert (
4649 "[TokenProtector] ⚠ OS LEAK! env '{}' = {} ({})" ,
47- entry . getKey () ,
50+ key ,
4851 isJwt ? "REAL JWT" : "possible token" ,
4952 val != null ? val .substring (0 , Math .min (30 , val .length ())) + "..." : "null" );
5053 }
@@ -102,28 +105,109 @@ private static void scanForOsLeaks() {
102105 }
103106 }
104107
108+ private static boolean shouldFlagEnvironmentVariable (String upperKey , String value ) {
109+ if (looksLikePathValue (value ) || looksLikeVersionValue (value )) {
110+ return false ;
111+ }
112+
113+ if (looksLikeTokenValue (value )) {
114+ return true ;
115+ }
116+
117+ return looksLikeSensitiveKey (upperKey ) && looksLikeCredentialValue (value );
118+ }
119+
105120 private static boolean looksLikeSensitiveKey (String upperKey ) {
106121 return upperKey .contains ("TOKEN" )
107- || upperKey .contains ("ACCESS" )
122+ || upperKey .contains ("API_KEY" )
123+ || upperKey .contains ("OPENAI" )
124+ || upperKey .contains ("SECRET" )
125+ || upperKey .contains ("SESSION" )
126+ || upperKey .contains ("BEARER" )
127+ || upperKey .contains ("JWT" )
128+ || upperKey .contains ("OAUTH" )
108129 || upperKey .contains ("MINECRAFT" )
109130 || upperKey .contains ("MOJANG" )
110131 || upperKey .contains ("MSA" )
111132 || upperKey .contains ("XBL" )
112133 || upperKey .contains ("XSTS" )
113- || upperKey .contains ("BEARER" )
114134 || upperKey .contains ("AUTH" );
115135 }
116136
117137 private static boolean looksLikeTokenValue (String value ) {
118138 if (value == null || value .isBlank ()) return false ;
119- if (looksLikeJwt (value )) return true ;
120139 String v = value .trim ();
121- return (v .length () >= 80 && (v .contains ("." ) || v .matches ("^[A-Za-z0-9_\\ -]+$" )));
140+
141+ if (looksLikePathValue (v ) || looksLikeVersionValue (v )) {
142+ return false ;
143+ }
144+
145+ if (looksLikeJwt (v )) return true ;
146+ if (looksLikeOpenAiKey (v )) return true ;
147+
148+ // Long opaque secrets are suspicious, but avoid flagging ordinary prose,
149+ // paths, and JVM argument lists.
150+ return v .length () >= 80
151+ && !v .contains (" " )
152+ && !v .contains ("\\ " )
153+ && !v .contains ("/" )
154+ && !v .contains ("=" )
155+ && v .matches ("^[A-Za-z0-9._\\ -]+$" );
122156 }
123157
124158 private static boolean looksLikeJwt (String value ) {
125159 if (value == null ) return false ;
126160 String v = value .trim ();
127- return v .startsWith ("eyJ" ) || v .matches ("^[A-Za-z0-9_\\ -]+\\ .[A-Za-z0-9_\\ -]+\\ .[A-Za-z0-9_\\ -]+$" );
161+ if (!v .startsWith ("eyJ" )) {
162+ return false ;
163+ }
164+
165+ String [] parts = v .split ("\\ ." );
166+ if (parts .length != 3 ) {
167+ return false ;
168+ }
169+
170+ // JWTs are three substantial base64url-ish segments, not short version numbers like 26.1.2.
171+ return parts [0 ].length () >= 8
172+ && parts [1 ].length () >= 8
173+ && parts [2 ].length () >= 8
174+ && parts [0 ].matches ("^[A-Za-z0-9_\\ -]+$" )
175+ && parts [1 ].matches ("^[A-Za-z0-9_\\ -]+$" )
176+ && parts [2 ].matches ("^[A-Za-z0-9_\\ -]+$" );
177+ }
178+
179+ private static boolean looksLikeOpenAiKey (String value ) {
180+ String v = value .trim ();
181+ return v .startsWith ("sk-" ) && v .length () >= 20 ;
182+ }
183+
184+ private static boolean looksLikeCredentialValue (String value ) {
185+ if (value == null || value .isBlank ()) return false ;
186+ String v = value .trim ();
187+ return looksLikeJwt (v )
188+ || looksLikeOpenAiKey (v )
189+ || (v .length () >= 24
190+ && !looksLikePathValue (v )
191+ && !looksLikeVersionValue (v )
192+ && !v .contains (" " )
193+ && v .matches ("^[A-Za-z0-9._\\ -]+$" ));
194+ }
195+
196+ private static boolean looksLikePathValue (String value ) {
197+ if (value == null ) return false ;
198+ String v = value .trim ();
199+ return v .contains ("\\ " )
200+ || v .contains (":/" )
201+ || v .matches ("^[A-Za-z]:\\ \\ .*" )
202+ || v .contains (";" )
203+ || v .startsWith ("-Duser." )
204+ || v .startsWith ("-X" )
205+ || v .startsWith ("--" );
206+ }
207+
208+ private static boolean looksLikeVersionValue (String value ) {
209+ if (value == null ) return false ;
210+ String v = value .trim ();
211+ return v .matches ("^\\ d+(\\ .\\ d+){1,3}$" );
128212 }
129213}
0 commit comments