Fixes to reduce the SSRF attack vector.

This commit is contained in:
Chris Hunt
2025-07-13 19:06:33 +01:00
parent 2df58472a1
commit 69bc8cb34e
14 changed files with 4278 additions and 8528 deletions

View File

@@ -65,3 +65,5 @@ AUTH_ROLES_HEADER="remote-groups"
AUTH_ROLES_HTTP_HEADER="HTTP_REMOTE_GROUPS"
AUTH_ROLES_ADMIN="admin"
AUTH_ROLES_DELIMITER=","
ALLOW_INTERNAL_REQUESTS=false

View File

@@ -21,6 +21,7 @@ use Illuminate\Support\Facades\URL;
use Illuminate\Validation\ValidationException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use Illuminate\Http\Response;
use enshrined\svgSanitize\Sanitizer;
class ItemController extends Controller
@@ -490,25 +491,48 @@ class ItemController extends Controller
*/
public function execute($url, array $attrs = [], $overridevars = false): ?ResponseInterface
{
$vars = ($overridevars !== false) ?
$overridevars : [
'http_errors' => false,
'timeout' => 15,
'connect_timeout' => 15,
'verify' => false,
];
// Default Guzzle client configuration
$clientOptions = [
'http_errors' => false,
'timeout' => 15,
'connect_timeout' => 15,
'verify' => false, // In production, set this to `true` and manage certs.
];
$client = new Client($vars);
// If the user provided overrides, use them.
if ($overridevars !== false) {
$clientOptions = $overridevars;
}
// Resolve the hostname to an IP address
$host = parse_url($url, PHP_URL_HOST);
$ip = gethostbyname($host);
// Check if the IP is private or reserved
$allowInternalIps = env('ALLOW_INTERNAL_REQUESTS', false);
if (!$allowInternalIps && filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
Log::warning('Blocked access to private or reserved IPs.', ['ip' => $ip, 'host' => $host]);
abort(Response::HTTP_FORBIDDEN, 'Access to private or reserved IPs is not allowed.');
}
// Force Guzzle to use the resolved IP address
$clientOptions['curl'][CURLOPT_RESOLVE] = ["{$host}:80:{$ip}", "{$host}:443:{$ip}"];
$client = new Client($clientOptions);
$method = 'GET';
try {
return $client->request($method, $url, $attrs);
} catch (ConnectException $e) {
Log::error('Connection refused');
Log::debug($e->getMessage());
Log::warning('SSRF Attempt Blocked: Connection to a private IP was prevented.', [
'url' => $url,
'error' => $e->getMessage()
]);
return null;
} catch (ServerException $e) {
Log::debug($e->getMessage());
} catch (\Exception $e) {
Log::error('General error: ' . $e->getMessage());
}
return null;
@@ -520,10 +544,22 @@ class ItemController extends Controller
*/
public function websitelookup($url): StreamInterface
{
$url = base64_decode($url);
$data = $this->execute($url);
$decodedUrl = base64_decode($url);
return $data->getBody();
// Validate the URL format.
if (filter_var($decodedUrl, FILTER_VALIDATE_URL) === false) {
abort(Response::HTTP_BAD_REQUEST, 'Invalid URL format provided.');
}
$response = $this->execute($decodedUrl);
// If execute() returns null, it means the connection failed.
// This can happen for many reasons, including our SSRF protection kicking in.
if ($response === null) {
abort(Response::HTTP_FORBIDDEN, 'Access to the requested resource is not allowed or the resource is unavailable.');
}
return $response->getBody();
}
/**

View File

@@ -9,6 +9,7 @@ return [
'appsource' => env('APP_SOURCE', 'https://appslist.heimdall.site/'),
'allow_internal_requests' => env('ALLOW_INTERNAL_REQUESTS', false),
'aliases' => Facade::defaultAliases()->merge([
'EnhancedApps' => App\EnhancedApps::class,

6813
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,12 +2,12 @@
"private": true,
"scripts": {
"dev": "npm run development",
"development": "mix",
"watch": "mix watch",
"watch-poll": "mix watch -- --watch-options-poll=1000",
"hot": "mix watch --hot",
"development": "npx mix",
"watch": "npx mix watch",
"watch-poll": "npx mix watch -- --watch-options-poll=1000",
"hot": "npx mix watch --hot",
"prod": "npm run production",
"production": "mix --production",
"production": "npx mix --production",
"lint": "eslint 'resources/assets/js/*'"
},
"devDependencies": {
@@ -21,7 +21,9 @@
"laravel-mix": "^6.0.49",
"prettier": "^2.8.1",
"sass": "^1.56.1",
"sass-loader": "13.*"
"sass-loader": "13.*",
"webpack": "^5.100.1",
"webpack-cli": "^6.0.1"
},
"dependencies": {
"select2": "^4.0.13",

4
public/css/app.css vendored

File diff suppressed because one or more lines are too long

2
public/js/app.js vendored

File diff suppressed because one or more lines are too long

1
public/js/dummy.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,5 @@
{
"/css/app.css": "/css/app.css?id=e8db4d3d99fdd3f0c747bb894c843e8e",
"/js/app.js": "/js/app.js?id=3b306fb20ef1a3c96c09963a4f6ff712"
"/js/dummy.js": "/js/dummy.js?id=daec5f3b283a510837bec36ca3868a54",
"/css/app.css": "/css/app.css?id=8e5c9ae35dd160a37c9d33d663f996b9",
"/js/app.js": "/js/app.js?id=19052619246fec368cad13937c62d850"
}

View File

@@ -183,6 +183,35 @@ openssl.cafile = /config/heimdall.pem
Restart the container and the Enhanced apps should now be able to access your local HTTP websites. This configuration will survive updating or recreating the Heimdall container.
## Allow Internal IP Requests
By default, Heimdall blocks requests to private or reserved IP addresses to mitigate potential security risks such as Server-Side Request Forgery (SSRF). However, you can enable access to internal IPs by setting the `ALLOW_INTERNAL_REQUESTS` environment variable in your `.env` file.
### Steps to Enable Internal IP Requests
1. Open your `.env` file located in the root directory of your Heimdall installation.
2. Add the following line:
```env
ALLOW_INTERNAL_REQUESTS=true
```
Setting this to `true` allows Heimdall to make requests to internal IP addresses (e.g., `192.168.x.x`, `10.x.x.x`, `127.0.0.1`).
3. Save the file and clear the Laravel configuration cache:
```bash
php artisan config:clear
```
4. Restart your web server or development server:
```bash
php artisan serve
```
### Default Behavior
If the `ALLOW_INTERNAL_REQUESTS` variable is not set or is set to `false`, Heimdall will block requests to private or reserved IP addresses and return a `403 Forbidden` error.
### Important Notes
- Enabling internal IP requests may expose your application to SSRF risks if your Heimdall instance is accessible from the internet. Ensure your instance is properly secured and not publicly accessible.
- Use this feature only if you trust the internal network and understand the security implications.
## Running offline
The apps list is hosted on github, you have a couple of options if you want to run without a connection to the outside world:
1) Clone the repository and host it yourself, look at the .github actions file to see how to generate the apps list.

View File

@@ -788,12 +788,20 @@ div.create {
width: 160px;
height: 160px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 1px solid #ccc;
border-radius: 4px;
cursor: pointer;
gap: 5px;
}
.icon-name {
font-size: 12px;
color: #666;
word-break: break-word;
text-align: center;
}
.selecticon {
max-width: 120px;
height: auto;

View File

@@ -153,7 +153,12 @@
$('#tile-preview .title').text($('#appname').val());
$('#websiteiconoptions').html('<div class="header"><span>Select Icon</span><span class="selectclose">Close</span></div><div class="results"></div>')
icons.forEach(icon => {
$('#websiteiconoptions .results').append('<div class="iconbutton"><img class="selecticon" src="' + icon + '" /></div>')
$('#websiteiconoptions .results').append(`
<div class="iconbutton">
<img class="selecticon" src="${icon}" />
<span class="icon-name">${icon.split('/').pop()}</span>
</div>
`);
})
console.log(websitedata)
})

1
webpack.mix.js vendored
View File

@@ -12,6 +12,7 @@ const mix = require("laravel-mix");
*/
mix
.js("resources/assets/js/app.js", "public/js/dummy.js")
.babel(
[
"node_modules/sortablejs/Sortable.min.js",

5857
yarn.lock

File diff suppressed because it is too large Load Diff