@@ -16,6 +16,68 @@ class File {
1616
1717 const MARKER = 'LiteSpeed Operator ' ;
1818
19+ /**
20+ * Whitelist .htaccess seeded into LITESPEED_STATIC_DIR. Anything not explicitly allowed returns 403.
21+ *
22+ * @since 7.9
23+ */
24+ const STATIC_DIR_HTACCESS = <<<HTACCESS
25+ # BEGIN LSCACHE_STATIC_PROTECT
26+ # DO NOT EDIT — auto-generated by LiteSpeed Cache.
27+ # Whitelist: only listed root files and folders are publicly readable.
28+
29+ Options -Indexes
30+
31+ <IfModule mod_rewrite.c>
32+ RewriteEngine On
33+
34+ # Block dotfiles (.litespeed_conf.dat, .htaccess, etc.) anywhere in the tree
35+ RewriteRule (^|/)\.[^/]+$ - [F,L]
36+
37+ # Whitelisted root-level public files
38+ RewriteRule ^robots\.txt$ - [L]
39+
40+ # Whitelisted public asset folders
41+ RewriteRule ^(css|js|ucss|ccss|lqip|optimax|avatar|localres|crawler)(/|$) - [L]
42+
43+ # Default deny
44+ RewriteRule .* - [F,L]
45+ </IfModule>
46+ # END LSCACHE_STATIC_PROTECT
47+ HTACCESS ;
48+
49+ /**
50+ * Seed .htaccess + robots.txt into LITESPEED_STATIC_DIR if missing
51+ *
52+ * @since 7.9
53+ */
54+ public static function ensure_static_protection () {
55+ if (!is_dir (LITESPEED_STATIC_DIR )) {
56+ return ;
57+ }
58+ $ htaccess_path = LITESPEED_STATIC_DIR . '/.htaccess ' ;
59+ if (is_file ($ htaccess_path )) {
60+ return ;
61+ }
62+ $ bytes = @file_put_contents ($ htaccess_path , self ::STATIC_DIR_HTACCESS , LOCK_EX );
63+
64+ $ robots_path = LITESPEED_STATIC_DIR . '/robots.txt ' ;
65+ if (!is_file ($ robots_path )) {
66+ @file_put_contents ($ robots_path , "User-agent: * \nDisallow: / \n" , LOCK_EX );
67+ }
68+
69+ $ banner = sprintf (
70+ /* translators: %s: full path to the .htaccess file LiteSpeed Cache failed to create. */
71+ __ ('LiteSpeed Cache could not create %s. Sensitive files (configuration backups, debug logs) under this folder may be publicly accessible. Fix the directory permissions to restore protection. ' , 'litespeed-cache ' ),
72+ '<code> ' . esc_html ($ htaccess_path ) . '</code> '
73+ );
74+ if ($ bytes === strlen (self ::STATIC_DIR_HTACCESS )) {
75+ Admin_Display::dismiss_pin_by_content ($ banner , Admin_Display::NOTICE_RED , true );
76+ } else {
77+ Admin_Display::error ($ banner , false , true );
78+ }
79+ }
80+
1981 /**
2082 * Detect if an URL is 404
2183 *
@@ -146,17 +208,17 @@ public static function save( $filename, $data, $mkdir = false, $append = false,
146208
147209 try {
148210 mkdir ($ folder , 0755 , true );
149- // Create robots.txt file to forbid search engine indexes
150- if (!file_exists (LITESPEED_STATIC_DIR . '/robots.txt ' )) {
151- file_put_contents (LITESPEED_STATIC_DIR . '/robots.txt ' , "User-agent: * \nDisallow: / \n" );
152- }
153211 } catch (\ErrorException $ ex ) {
154212 return $ silence ? false : sprintf (__ ('Can not create folder: %1$s. Error: %2$s ' , 'litespeed-cache ' ), $ folder , $ ex ->getMessage ());
155213 }
156214
157215 restore_error_handler ();
158216 }
159217
218+ if (strpos ($ filename , LITESPEED_STATIC_DIR . '/ ' ) === 0 ) {
219+ self ::ensure_static_protection ();
220+ }
221+
160222 if (!file_exists ($ filename )) {
161223 if (!is_writable ($ folder )) {
162224 return $ silence ? false : sprintf (__ ('Folder is not writable: %s. ' , 'litespeed-cache ' ), $ folder );
0 commit comments