diff --git a/.htaccess b/.htaccess
index a1dafd3..04ccfec 100644
--- a/.htaccess
+++ b/.htaccess
@@ -36,6 +36,11 @@ Options -Indexes
# /services/ → /services (fixes broken relative CSS under trailing slash)
RewriteRule ^services/$ /services [R=301,L]
+ # Nested extensionless pages: /services/foo/ → /services/foo
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteCond %{DOCUMENT_ROOT}/$1.html -f
+ RewriteRule ^(.+)/$ /$1 [R=301,L]
+
# /services → services.html (page, not the services/ directory)
RewriteRule ^services$ services.html [L]
@@ -48,6 +53,10 @@ Options -Indexes
# Safe security headers (do not block static assets)
- Header set X-Content-Type-Options "nosniff"
- Header set Referrer-Policy "strict-origin-when-cross-origin"
+ Header always set X-Content-Type-Options "nosniff"
+ Header always set Referrer-Policy "strict-origin-when-cross-origin"
+ Header always set X-Frame-Options "SAMEORIGIN"
+ Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
+ Header always set Content-Security-Policy "frame-ancestors 'self'; base-uri 'self'; object-src 'none'; form-action 'self'"
+ Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
diff --git a/404.html b/404.html
index 65d8949..0b1d0f1 100644
--- a/404.html
+++ b/404.html
@@ -12,7 +12,7 @@
-
+
diff --git a/about.html b/about.html
index a51d7fe..ea66bf5 100644
--- a/about.html
+++ b/about.html
@@ -11,7 +11,7 @@
-
+
diff --git a/api/careers.php b/api/careers.php
index 98fe2e7..a1814e4 100644
--- a/api/careers.php
+++ b/api/careers.php
@@ -13,6 +13,9 @@ if (zs_is_honeypot_filled()) {
zs_json_response(200, ['ok' => true, 'message' => 'Thank you. Your application has been received.']);
}
+zs_require_csrf();
+zs_rate_limit('careers');
+
$name = zs_field('name', 120);
$email = zs_field('email', 160);
$phone = zs_field('phone', 40);
diff --git a/api/contact.php b/api/contact.php
index 892fc7d..53fe137 100644
--- a/api/contact.php
+++ b/api/contact.php
@@ -13,6 +13,9 @@ if (zs_is_honeypot_filled()) {
zs_json_response(200, ['ok' => true, 'message' => 'Thank you. We have received your message.']);
}
+zs_require_csrf();
+zs_rate_limit('contact');
+
$name = zs_field('name', 120);
$email = zs_field('email', 160);
$phone = zs_field('phone', 40);
diff --git a/api/helpers.php b/api/helpers.php
index 0bc5825..2b1bd96 100644
--- a/api/helpers.php
+++ b/api/helpers.php
@@ -10,6 +10,8 @@ const ZS_CAREERS_TO = 'careers@zettaisystems.com';
const ZS_FROM_NAME = 'Zettai Systems Website';
const ZS_FROM_EMAIL = 'noreply@zettaisystems.com';
const ZS_MAX_RESUME_BYTES = 5 * 1024 * 1024; // 5 MB
+const ZS_RL_MAX = 5;
+const ZS_RL_WINDOW = 900; // 15 minutes
function zs_json_response(int $status, array $payload): void
{
@@ -27,6 +29,100 @@ function zs_require_post(): void
}
}
+function zs_request_host(): string
+{
+ $host = $_SERVER['HTTP_HOST'] ?? '';
+ if (!is_string($host) || $host === '') {
+ return '';
+ }
+ return strtolower((string) preg_replace('/:\d+$/', '', $host));
+}
+
+function zs_require_csrf(): void
+{
+ $host = zs_request_host();
+ $origin = $_SERVER['HTTP_ORIGIN'] ?? '';
+ if (is_string($origin) && $origin !== '') {
+ $originHost = parse_url($origin, PHP_URL_HOST);
+ if (!is_string($originHost) || strtolower($originHost) !== $host) {
+ zs_json_response(403, [
+ 'ok' => false,
+ 'error' => 'Security check failed. Please refresh the page and try again.',
+ ]);
+ }
+ }
+
+ $cookie = $_COOKIE['zs_csrf'] ?? '';
+ $posted = $_POST['csrf_token'] ?? '';
+ if (
+ !is_string($cookie) ||
+ !is_string($posted) ||
+ $cookie === '' ||
+ $posted === '' ||
+ !hash_equals($cookie, $posted)
+ ) {
+ zs_json_response(403, [
+ 'ok' => false,
+ 'error' => 'Security check failed. Please refresh the page and try again.',
+ ]);
+ }
+}
+
+function zs_rate_limit(string $bucket): void
+{
+ $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
+ if (!is_string($ip) || $ip === '') {
+ $ip = '0.0.0.0';
+ }
+
+ $dir = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'zs_form_rl';
+ if (!is_dir($dir) && !@mkdir($dir, 0700, true) && !is_dir($dir)) {
+ return;
+ }
+
+ $file = $dir . DIRECTORY_SEPARATOR . hash('sha256', $bucket . '|' . $ip);
+ $fh = @fopen($file, 'c+');
+ if ($fh === false) {
+ return;
+ }
+
+ flock($fh, LOCK_EX);
+ $raw = stream_get_contents($fh);
+ $hits = [];
+ if (is_string($raw) && $raw !== '') {
+ $decoded = json_decode($raw, true);
+ if (is_array($decoded)) {
+ $hits = $decoded;
+ }
+ }
+
+ $now = time();
+ $windowStart = $now - ZS_RL_WINDOW;
+ $fresh = [];
+ foreach ($hits as $stamp) {
+ if (is_int($stamp) && $stamp > $windowStart) {
+ $fresh[] = $stamp;
+ }
+ }
+
+ if (count($fresh) >= ZS_RL_MAX) {
+ flock($fh, LOCK_UN);
+ fclose($fh);
+ zs_json_response(429, [
+ 'ok' => false,
+ 'error' => 'Too many submissions. Please wait a few minutes and try again.',
+ ]);
+ }
+
+ $fresh[] = $now;
+ rewind($fh);
+ ftruncate($fh, 0);
+ fwrite($fh, json_encode($fresh));
+ fflush($fh);
+ flock($fh, LOCK_UN);
+ fclose($fh);
+}
+
function zs_field(string $key, int $max = 2000): string
{
$value = $_POST[$key] ?? '';
diff --git a/careers.html b/careers.html
index 6c239e7..ec4467a 100644
--- a/careers.html
+++ b/careers.html
@@ -11,7 +11,7 @@
-
+
diff --git a/contact.html b/contact.html
index b705358..649d68d 100644
--- a/contact.html
+++ b/contact.html
@@ -11,7 +11,7 @@
-
+
diff --git a/css/style.css b/css/style.css
index 138a71b..733b9db 100644
--- a/css/style.css
+++ b/css/style.css
@@ -2404,6 +2404,46 @@ body[data-page="contact"] .back-to-top {
top: 1rem;
}
+.cookie-banner {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 2100;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.85rem 1.25rem;
+ padding: 0.95rem 1.25rem;
+ background: var(--surface-elevated);
+ color: var(--text-color);
+ border-top: 1px solid var(--border-color);
+}
+
+.cookie-banner p {
+ margin: 0;
+ flex: 1 1 16rem;
+ font-size: 0.92rem;
+ color: var(--muted-text-color);
+}
+
+.cookie-banner a {
+ color: var(--primary-color);
+ text-decoration: underline;
+}
+
+.cookie-banner-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+}
+
+body.has-cookie-banner .floating-cta,
+body.has-cookie-banner .back-to-top {
+ display: none !important;
+}
+
/* Service detail extras */
.benefit-list {
list-style: none;
diff --git a/index.html b/index.html
index f2bc1f7..69cb6cb 100644
--- a/index.html
+++ b/index.html
@@ -11,7 +11,7 @@
-
+
@@ -113,7 +113,7 @@
Secure Architecture
Cloud-Ready Delivery
-