Fix open redirect security issue (CWE-601)
[surrogator.git] / www / avatar.php
1 <?php
2 /**
3  * Script that handles avatar image requests.
4  *
5  * Part of Surrogator - a simple libravatar avatar image server.
6  *
7  * PHP version 5
8  *
9  * @category Tools
10  * @package  Surrogator
11  * @author   Christian Weiske <cweiske@cweiske.de>
12  * @license  http://www.gnu.org/licenses/agpl.html AGPLv3 or later
13  * @link     https://sourceforge.net/p/surrogator/
14  */
15 namespace surrogator;
16 $cfgFile = __DIR__ . '/../data/surrogator.config.php';
17 if (!file_exists($cfgFile)) {
18     $cfgFile = '/etc/surrogator.config.php';
19     if (!file_exists($cfgFile)) {
20         err(
21             500,
22             "Configuration file does not exist.",
23             "Copy data/surrogator.config.php.dist to data/surrogator.config.php"
24         );
25         exit(2);
26     }
27 }
28 require $cfgFile;
29
30 /**
31  * Send an error message out.
32  *
33  * @param integer $statusCode HTTP status code
34  * @param string  $msg        Error message
35  *
36  * @return void
37  */
38 function err($statusCode, $msg, $more = '')
39 {
40     header('HTTP/1.0 ' . $statusCode . ' ' . $msg);
41     header('Content-Type: text/plain');
42     echo $msg . "\n" . $more;
43     exit(1);
44 }
45
46 $uri = $_SERVER['REQUEST_URI'];
47 $uriParts = explode('/', $uri, 3);
48 if (count($uriParts) != 3 || $uriParts[1] != 'avatar') {
49     err(400, 'URI is wrong, should be avatar/$hash');
50 }
51 $reqHash = $uriParts[2];
52 if (strpos($reqHash, '?') !== false) {
53     $reqHash = substr($reqHash, 0, strpos($reqHash, '?'));
54 }
55 if (strlen($reqHash) !== 32 && strlen($reqHash) !== 64) {
56     err(400, 'Hash has to be 32 or 64 characters long');
57 }
58
59 $reqSize = 80;//default
60 if (isset($_GET['s'])) {
61     $_GET['size'] = $_GET['s'];
62 }
63 if (isset($_GET['size'])) {
64     if ($_GET['size'] != intval($_GET['size'])) {
65         err(400, 'size parameter is not an integer');
66     }
67     if ($_GET['size'] < 1) {
68         err(400, 'size parameter has to be larger than 0');
69     }
70     $reqSize = intval($_GET['size']);
71 }
72
73 $default     = 'default.png';
74 $defaultMode = 'local';
75 if (isset($_GET['d'])) {
76     $_GET['default'] = $_GET['d'];
77 }
78 if (isset($_GET['default'])) {
79     if ($_GET['default'] == '') {
80         err(400, 'default parameter is empty');
81     } else if (preg_match('#^[a-z0-9]+$#', $_GET['default'])) {
82         //special default mode, we support none of them except 404
83         if ($_GET['default'] == '404') {
84             $defaultMode = '404';
85             $default     = '404';
86         } else if ($_GET['default'] == 'mm') {
87             //mystery man fallback image
88             $defaultMode = 'local';
89             $default     = 'mm.png';
90         } else {
91             //local default image
92             $defaultMode = 'local';
93             $default     = 'default.png';
94         }
95     } else {
96         //url
97         $defaultMode = 'redirect';
98         $default     = $_GET['default'];
99
100         $allowed = false;
101         foreach ($trustedDefaultUrls ?? [] as $urlPrefix) {
102             if (substr($default, 0, strlen($urlPrefix))  == $urlPrefix) {
103                 $allowed = true;
104                 break;
105             }
106         }
107         if (!$allowed) {
108             header('X-Info: default parameter URL not allowed');
109             $defaultMode = 'local';
110             $default     = 'default.png';
111         }
112     }
113 }
114
115
116 $targetSize = 512;
117 foreach ($sizes as $size) {
118     if ($reqSize <= $size) {
119         $targetSize = $size;
120         break;
121     }
122 }
123
124 $imgFile = $varDir . $targetSize . '/' . $reqHash . '.png';
125 if (!file_exists($imgFile)) {
126     if ($defaultMode == '404') {
127         err(404, 'File does not exist');
128     } else if ($defaultMode == 'redirect') {
129         header('Location: ' . $default);
130         exit();
131     } else if ($defaultMode == 'local') {
132         $imgFile = $varDir . $targetSize . '/' . $default;
133         if (!file_exists($imgFile)) {
134             err(500, 'Default file is missing');
135         }
136     } else {
137         err(500, 'Invalid defaultMode');
138     }
139 }
140
141 $stat = stat($imgFile);
142 $etag = sprintf('%x-%x-%x', $stat['ino'], $stat['size'], $stat['mtime'] * 1000000);
143
144 if (isset($_SERVER['HTTP_IF_NONE_MATCH'])
145     && $_SERVER['HTTP_IF_NONE_MATCH'] == $etag
146 ) {
147     header('Etag: "' . $etag . '"');
148     header('HTTP/1.0 304 Not Modified');
149     exit();
150 } else if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])
151     && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) >= $stat['mtime']
152 ) {
153     header('Last-Modified: ' . date('r', $stat['mtime']));
154     header('HTTP/1.0 304 Not Modified');
155     exit();
156 }
157
158 header('Last-Modified: ' . date('r', $stat['mtime']));
159 header('Etag: "' . $etag . '"');
160 header('Content-Type: image/png');
161 header('Content-Length:' . $stat['size']);
162
163 readfile($imgFile);
164 ?>