@@ -128,22 +128,30 @@ public static function curlGetContents(
128128
129129 public static function redirect (string $ url , int $ time = 0 , string $ msg = '' ): never
130130 {
131- $ url = str_replace (["\n" , "\r" ], '' , $ url );
131+ $ url = str_replace (["\n" , "\r" , "\0" ], '' , $ url );
132+
133+ // Block javascript: and data: URIs which cannot be safe redirect targets
134+ if (preg_match ('/^\s*(?:javascript|data|vbscript):/i ' , $ url )) {
135+ $ url = '/ ' ;
136+ }
137+
132138 if (empty ($ msg )) {
133- $ msg = "系统将在 {$ time }秒之后自动跳转到 {$ url }! " ;
139+ $ escapedUrl = htmlspecialchars ($ url , ENT_QUOTES | ENT_SUBSTITUTE , 'UTF-8 ' );
140+ $ msg = "系统将在 {$ time }秒之后自动跳转到 {$ escapedUrl }! " ;
134141 }
135142 if (!headers_sent ()) {
136143 if (0 === $ time ) {
137144 header ('Location: ' . $ url );
138145 } else {
139146 header ("refresh: {$ time };url= {$ url }" );
140- echo $ msg ;
147+ echo htmlspecialchars ( $ msg, ENT_QUOTES | ENT_SUBSTITUTE , ' UTF-8 ' ) ;
141148 }
142149 exit ();
143150 } else {
144- $ str = "<meta http-equiv='Refresh' content=' {$ time };URL= {$ url }'> " ;
151+ $ safeUrl = htmlspecialchars ($ url , ENT_QUOTES | ENT_SUBSTITUTE , 'UTF-8 ' );
152+ $ str = "<meta http-equiv='Refresh' content=' {$ time };URL= {$ safeUrl }'> " ;
145153 if ($ time != 0 ) {
146- $ str .= $ msg ;
154+ $ str .= htmlspecialchars ( $ msg, ENT_QUOTES | ENT_SUBSTITUTE , ' UTF-8 ' ) ;
147155 }
148156 exit ($ str );
149157 }
@@ -245,7 +253,7 @@ public static function signature(array $datas, string $key): string
245253 $ tmp [] = $ k . '= ' . $ v ;
246254 }
247255 }
248- return md5 ( implode ('& ' , $ tmp ) . ' &key= ' . $ key );
256+ return hash_hmac ( ' sha256 ' , implode ('& ' , $ tmp ), $ key );
249257 }
250258
251259 public static function htmlFilter (string $ html ): string
@@ -260,56 +268,56 @@ public static function htmlFilter(string $html): string
260268 }
261269
262270 /**
263- * @deprecated since PlumePHP 1.4.0 — uses MD5-based RC4 stream cipher (Discuz legacy).
264- * Replace with sodium_crypto_secretbox() or openssl_encrypt('AES-256-GCM', ...).
265- * This function will be removed in a future major version.
271+ * Encrypt or decrypt a string using AES-256-GCM (authenticated encryption).
272+ *
273+ * Encoding: base64url(nonce[12] + tag[16] + ciphertext) for ENCODE,
274+ * or the same layout for DECODE.
275+ *
276+ * The $expiry parameter is embedded as a 10-digit Unix timestamp prefix
277+ * inside the plaintext (0 = no expiry), preserving backward-compatible
278+ * semantics with the legacy RC4-based implementation this replaced.
279+ *
280+ * @param string $string Plaintext (ENCODE) or ciphertext (DECODE)
281+ * @param string $operation 'ENCODE' or 'DECODE'
282+ * @param string $key Secret key (min 16 chars); falls back to APP_SECRET
283+ * @param int $expiry Seconds until the token expires (0 = forever)
284+ * @return string Ciphertext on ENCODE, plaintext on DECODE, '' on failure
266285 */
267286 public static function authcode (string $ string , string $ operation = 'DECODE ' , string $ key = '' , int $ expiry = 0 ): string
268287 {
269- $ ckeyLength = 4 ;
270- $ key = md5 ($ key ?: 'plumephp ' );
271- $ keya = md5 (substr ($ key , 0 , 16 ));
272- $ keyb = md5 (substr ($ key , 16 , 16 ));
273- // @phpstan-ignore-next-line ($ckeyLength is intentionally a constant 4 here)
274- $ keyc = $ ckeyLength
275- ? ($ operation == 'DECODE ' ? substr ($ string , 0 , $ ckeyLength ) : substr (md5 (microtime ()), -$ ckeyLength ))
276- : '' ;
277- $ cryptkey = $ keya . md5 ($ keya . $ keyc );
278- $ keyLength = strlen ($ cryptkey );
279- $ string = $ operation == 'DECODE '
280- ? base64_decode (substr ($ string , $ ckeyLength ))
281- : sprintf ('%010d ' , $ expiry ? $ expiry + time () : 0 ) . substr (md5 ($ string . $ keyb ), 0 , 16 ) . $ string ;
282- $ stringLength = strlen ($ string );
283- $ result = '' ;
284- $ box = range (0 , 255 );
285- $ rndkey = [];
286- for ($ i = 0 ; $ i <= 255 ; $ i ++) {
287- $ rndkey [$ i ] = ord ($ cryptkey [$ i % $ keyLength ]);
288- }
289- for ($ j = $ i = 0 ; $ i < 256 ; $ i ++) {
290- $ j = ($ j + $ box [$ i ] + $ rndkey [$ i ]) % 256 ;
291- $ tmp = $ box [$ i ];
292- $ box [$ i ] = $ box [$ j ];
293- $ box [$ j ] = $ tmp ;
294- }
295- for ($ a = $ j = $ i = 0 ; $ i < $ stringLength ; $ i ++) {
296- $ a = ($ a + 1 ) % 256 ;
297- $ j = ($ j + $ box [$ a ]) % 256 ;
298- $ tmp = $ box [$ a ];
299- $ box [$ a ] = $ box [$ j ];
300- $ box [$ j ] = $ tmp ;
301- $ result .= chr ((int )(ord ($ string [$ i ]) ^ ($ box [($ box [$ a ] + $ box [$ j ]) % 256 ])));
302- }
303- if ($ operation == 'DECODE ' ) {
304- $ expireTs = (int ) substr ($ result , 0 , 10 );
305- if (($ expireTs === 0 || $ expireTs - time () > 0 )
306- && substr ($ result , 10 , 16 ) == substr (md5 (substr ($ result , 26 ) . $ keyb ), 0 , 16 )
307- ) {
308- return substr ($ result , 26 );
288+ $ rawKey = $ key ?: (string ) getenv ('APP_SECRET ' ) ?: 'plumephp-insecure-fallback-key!! ' ;
289+ // Derive a 256-bit key via SHA-256 so any key length is accepted safely
290+ $ derivedKey = hash ('sha256 ' , $ rawKey , true );
291+
292+ if (strtoupper ($ operation ) === 'ENCODE ' ) {
293+ $ expireTs = $ expiry > 0 ? sprintf ('%010d ' , time () + $ expiry ) : '0000000000 ' ;
294+ $ plaintext = $ expireTs . $ string ;
295+ $ nonce = random_bytes (12 );
296+ $ tag = '' ;
297+ $ cipher = openssl_encrypt ($ plaintext , 'aes-256-gcm ' , $ derivedKey , OPENSSL_RAW_DATA , $ nonce , $ tag , '' , 16 );
298+ if ($ cipher === false ) {
299+ return '' ;
309300 }
301+ return rtrim (strtr (base64_encode ($ nonce . $ tag . $ cipher ), '+/ ' , '-_ ' ), '= ' );
302+ }
303+
304+ // DECODE
305+ $ raw = base64_decode (strtr ($ string , '-_ ' , '+/ ' ) . str_repeat ('= ' , (4 - strlen ($ string ) % 4 ) % 4 ), true );
306+ if ($ raw === false || strlen ($ raw ) < 29 ) { // 12 nonce + 16 tag + 1 min payload
310307 return '' ;
311308 }
312- return $ keyc . str_replace ('= ' , '' , base64_encode ($ result ));
309+ $ nonce = substr ($ raw , 0 , 12 );
310+ $ tag = substr ($ raw , 12 , 16 );
311+ $ cipher = substr ($ raw , 28 );
312+ $ plain = openssl_decrypt ($ cipher , 'aes-256-gcm ' , $ derivedKey , OPENSSL_RAW_DATA , $ nonce , $ tag );
313+ if ($ plain === false ) {
314+ return '' ;
315+ }
316+ $ expireTs = (int ) substr ($ plain , 0 , 10 );
317+ if ($ expireTs !== 0 && $ expireTs < time ()) {
318+ return '' ;
319+ }
320+ return substr ($ plain , 10 );
313321 }
314322
315323 // -----------------------------------------------------------------------
@@ -335,10 +343,20 @@ public static function arrayMergeDeep(array &$arr1, array $arr2): void
335343
336344 public static function uuid (string $ prefix = '' ): string
337345 {
338- $ time = md5 (microtime ());
339- $ rand1 = md5 (substr ($ time , rand (0 , 10 ), rand (22 , 32 )));
340- $ rand2 = md5 (substr ($ rand1 , rand (0 , 10 ), rand (22 , 32 )));
341- return strtolower (md5 ($ prefix . uniqid ($ prefix ) . $ time . $ rand1 . $ rand2 ));
346+ $ bytes = random_bytes (16 );
347+ // Set version 4 (random) and variant bits per RFC 4122
348+ $ bytes [6 ] = chr ((ord ($ bytes [6 ]) & 0x0f ) | 0x40 );
349+ $ bytes [8 ] = chr ((ord ($ bytes [8 ]) & 0x3f ) | 0x80 );
350+ $ hex = bin2hex ($ bytes );
351+ $ uuid = sprintf (
352+ '%s-%s-%s-%s-%s ' ,
353+ substr ($ hex , 0 , 8 ),
354+ substr ($ hex , 8 , 4 ),
355+ substr ($ hex , 12 , 4 ),
356+ substr ($ hex , 16 , 4 ),
357+ substr ($ hex , 20 , 12 )
358+ );
359+ return $ prefix ? $ prefix . '- ' . $ uuid : $ uuid ;
342360 }
343361
344362 public static function strcut (string $ str , int $ len , string $ ext = '' , int $ zhLen = 0 ): string
0 commit comments