Completed
Push — master ( 036b0c...d61dcf )
by Derek Stephen
01:27
created

SessionManager::isHijackAttempt()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 19
rs 9.3222
c 0
b 0
f 0
cc 5
nc 4
nop 0
1
<?php declare(strict_types=1);
2
3
namespace Del;
4
5
final class SessionManager
6
{
7
    const IP_REGEX = '/(\d{1,3}\.\d{1,3}\.\d{1,3}\.)(\d{1,3})/';
8
9
    /**
10
     *  As this is a singleton, construction and clone are disabled
11
     *  use SessionManager::getInstance() if you need the instance
12
     */
13
    private function __construct()
14
    {
15
    }
16
17
    private function __clone()
18
    {
19
    }
20
21
    /**
22
     * @return SessionManager
23
     */
24
    public static function getInstance(): SessionManager
25
    {
26
        static $inst = null;
27
28
        if ($inst === null) {
29
            $inst = new SessionManager();
30
        }
31
32
        return $inst;
33
    }
34
35
    /**
36
     * Creates a secure session
37
     *
38
     * @param string $name
39
     * @param int $lifetime
40
     * @param string $path
41
     * @param string $domain
42
     * @param bool|null $secure
43
     */
44
    public static function sessionStart(string $name, int $lifetime = 0, string $path = '/', string $domain = '', ?bool $secure = null): void
45
    {
46
        // get the instance of the session manager
47
        $inst = self::getInstance();
48
49
        // Set the domain to default to the current domain.
50
        $domain = isset($domain) ? $domain : $_SERVER['SERVER_NAME'];
51
52
        // Set the default secure value to whether the site is being accessed with SSL
53
        $secure = isset($secure) ? $secure : isset($_SERVER['HTTPS']);
54
        $id = session_id();
55
        
56
        if (empty($id)) {
57
            session_name($name . '_Session');
58
            session_set_cookie_params($lifetime, $path, $domain, $secure, true);
59
            session_start();
60
        }
61
62
        // Make sure the session hasn't expired, and destroy it if it has
63
        $inst->isValid() ? $inst->initialise() :  $inst->destroySession();
64
65
    }
66
67
    private function initialise(): void
68
    {
69
        // Check to see if the session is a hijacking attempt
70
        if ($inst->isHijackAttempt()) {
71
72
            // Reset session data and regenerate id
73
            $_SESSION = [];
74
            $_SESSION['ipAddress'] = $inst->getIpAddress();
0 ignored issues
show
Bug introduced by
The variable $inst does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
75
            $_SESSION['userAgent'] = $_SERVER['HTTP_USER_AGENT'] ?? '';
76
            $inst->regenerateSession();
77
78
            return;
79
        }
80
81
        // Give a 5% chance of the session id changing on any request
82
        if ($inst->shouldRandomlyRegenerate()) {
83
            $inst->regenerateSession();
84
        }
85
    }
86
87
    /**
88
     * @return bool
89
     */
90
    private function shouldRandomlyRegenerate(): bool
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
91
    {
92
        return rand(1, 100) <= 5;
93
    }
94
95
96
    /**
97
     * Checks session IP and user agent are still the same
98
     * @return bool
99
     */
100
    private function isHijackAttempt(): bool
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
101
    {
102
        $ipAddress = $this->getIpAddress();
103
        $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
104
105
        if (!isset($_SESSION['ipAddress']) || !isset($_SESSION['userAgent'])) {
106
            return true;
107
        }
108
109
        if ($_SESSION['ipAddress'] != $ipAddress) {
110
            return true;
111
        }
112
113
        if ($_SESSION['userAgent'] !== $userAgent) {
114
            return true;
115
        }
116
117
        return false;
118
    }
119
120
    /**
121
     * If a site goes through the likes of Cloudflare, the last part of the IP might change
122
     * So we replace it with an x.
123
     *
124
     * @return string
125
     */
126
    private function getIpAddress(): string
127
    {
128
        $remoteAddress = $_SERVER['REMOTE_ADDR'] ?? '';
129
130
        return preg_replace(self::IP_REGEX, '$1x', $remoteAddress);
131
    }
132
133
    /**
134
     *  Creates a fresh session Id to make it harder to hack
135
     *  If the site is very slow in parts increase the expiry time
136
     *  10 seconds is a good default which allows ajax calls to work
137
     *  without losing the session
138
     */
139
    private function regenerateSession()
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
140
    {
141
        // If this session is obsolete it means there already is a new id
142
        if (isset($_SESSION['OBSOLETE']) && $_SESSION['OBSOLETE'] == true) {
143
            return;
144
        }
145
146
        // Set current session to expire in 10 seconds
147
        $_SESSION['OBSOLETE'] = true;
148
        $_SESSION['EXPIRES'] = time() + 10;
149
150
        // Create new session without destroying the old one
151
        session_regenerate_id(false);
152
153
        // Grab current session ID and close both sessions to allow other scripts to use them
154
        $newSession = session_id();
155
        session_write_close();
156
157
        // Set session ID to the new one, and start it back up again
158
        session_id($newSession);
159
        session_start();
160
161
        // Now we unset the obsolete and expiration values for the session we want to keep
162
        unset($_SESSION['OBSOLETE']);
163
        unset($_SESSION['EXPIRES']);
164
    }
165
166
    /**
167
     * Checks whether the session has expired or not
168
     * @return bool
169
     */
170
    private function isValid(): bool
171
    {
172
        if (isset($_SESSION['OBSOLETE']) && !isset($_SESSION['EXPIRES'])) {
173
            return false;
174
        }
175
176
        if (isset($_SESSION['EXPIRES']) && $_SESSION['EXPIRES'] < time()) {
177
            return false;
178
        }
179
180
        return true;
181
    }
182
183
    /**
184
     *  Resets the session
185
     */
186
    public static function destroySession()
187
    {
188
        $id = session_id();
189
190
        if (!empty($id)) {
191
            $_SESSION = [];
192
            session_destroy();
193
            session_start();
194
        }
195
    }
196
197
    /**
198
     * @param string $key
199
     * @param mixed $val
200
     */
201
    public function set(string $key, $val): void
202
    {
203
        $_SESSION[$key] = $val;
204
    }
205
206
    /**
207
     * @param $key
208
     * @return null|mixed
209
     */
210
    public function get(string $key)
211
    {
212
        return isset($_SESSION[$key]) ? $_SESSION[$key] : null;
213
    }
214
215
    /**
216
     * @param string $key
217
     * @return bool
218
     */
219
    public function has(string $key): bool
220
    {
221
        return isset($_SESSION[$key]);
222
    }
223
224
    /**
225
     * @param $key
226
     * @param $val
227
     */
228
    public function unset(string $key): void
229
    {
230
        unset($_SESSION[$key]);
231
    }
232
233
    /**
234
     * @param $key
235
     * @param $val
236
     * @deprecated use unset
237
     */
238
    public function destroy(string $key): void
239
    {
240
        unset($_SESSION[$key]);
241
    }
242
}
243