mirror of
https://github.com/linuxserver/Heimdall.git
synced 2025-10-26 02:33:53 +09:00
Fixes to reduce the SSRF attack vector.
This commit is contained in:
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
6813
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -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
4
public/css/app.css
vendored
File diff suppressed because one or more lines are too long
2
public/js/app.js
vendored
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
1
public/js/dummy.js
vendored
Normal file
File diff suppressed because one or more lines are too long
5
public/mix-manifest.json
generated
5
public/mix-manifest.json
generated
@@ -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"
|
||||
}
|
||||
|
||||
29
readme.md
29
readme.md
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
1
webpack.mix.js
vendored
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user