fix: harden fetching on upload endpoint (#14012)
This commit is contained in:
@@ -39,6 +39,11 @@ RSpec.describe 'Api::V1::Accounts::UploadController', type: :request do
|
||||
let(:valid_external_url) { 'http://example.com/image.jpg' }
|
||||
|
||||
before do
|
||||
allow(Resolv).to receive(:getaddresses).and_call_original
|
||||
allow(Resolv).to receive(:getaddresses).with('example.com').and_return(['93.184.216.34'])
|
||||
allow(Resolv).to receive(:getaddresses).with('error.example.com').and_return(['93.184.216.34'])
|
||||
allow(Resolv).to receive(:getaddresses).with('nonexistent.example.com').and_return(['93.184.216.34'])
|
||||
|
||||
stub_request(:get, valid_external_url)
|
||||
.to_return(status: 200, body: File.new(Rails.root.join('spec/assets/avatar.png')), headers: { 'Content-Type' => 'image/png' })
|
||||
end
|
||||
@@ -82,7 +87,7 @@ RSpec.describe 'Api::V1::Accounts::UploadController', type: :request do
|
||||
params: { external_url: 'http://nonexistent.example.com' }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to eq('Invalid URL provided')
|
||||
expect(response.parsed_body['error']).to eq('Failed to fetch file from URL')
|
||||
end
|
||||
|
||||
it 'handles HTTP errors' do
|
||||
@@ -96,6 +101,112 @@ RSpec.describe 'Api::V1::Accounts::UploadController', type: :request do
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to start_with('Failed to fetch file from URL')
|
||||
end
|
||||
|
||||
it 'rejects oversized responses with a file-size message' do
|
||||
stub_request(:get, valid_external_url)
|
||||
.to_return(status: 200,
|
||||
body: 'x' * (41 * 1024 * 1024),
|
||||
headers: { 'Content-Type' => 'image/png' })
|
||||
|
||||
post upload_url,
|
||||
headers: user.create_new_auth_token,
|
||||
params: { external_url: valid_external_url }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to eq('File exceeds the maximum allowed size')
|
||||
end
|
||||
|
||||
it 'rejects unsupported content types with a file-type message' do
|
||||
stub_request(:get, valid_external_url)
|
||||
.to_return(status: 200,
|
||||
body: '<html></html>',
|
||||
headers: { 'Content-Type' => 'text/html' })
|
||||
|
||||
post upload_url,
|
||||
headers: user.create_new_auth_token,
|
||||
params: { external_url: valid_external_url }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to eq('File type not supported (only images and videos are allowed)')
|
||||
end
|
||||
|
||||
context 'with SSRF attack vectors' do
|
||||
it 'blocks requests to private IP ranges (10.x.x.x)' do
|
||||
post upload_url,
|
||||
headers: user.create_new_auth_token,
|
||||
params: { external_url: 'http://10.0.0.1/secret' }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to eq('Invalid URL provided')
|
||||
end
|
||||
|
||||
it 'blocks requests to private IP ranges (172.16.x.x)' do
|
||||
post upload_url,
|
||||
headers: user.create_new_auth_token,
|
||||
params: { external_url: 'http://172.16.0.1/secret' }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to eq('Invalid URL provided')
|
||||
end
|
||||
|
||||
it 'blocks requests to private IP ranges (192.168.x.x)' do
|
||||
post upload_url,
|
||||
headers: user.create_new_auth_token,
|
||||
params: { external_url: 'http://192.168.1.1/secret' }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to eq('Invalid URL provided')
|
||||
end
|
||||
|
||||
it 'blocks requests to loopback addresses' do
|
||||
post upload_url,
|
||||
headers: user.create_new_auth_token,
|
||||
params: { external_url: 'http://127.0.0.1/secret' }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to eq('Invalid URL provided')
|
||||
end
|
||||
|
||||
it 'blocks requests to AWS metadata service (169.254.169.254)' do
|
||||
post upload_url,
|
||||
headers: user.create_new_auth_token,
|
||||
params: { external_url: 'http://169.254.169.254/latest/meta-data/iam/security-credentials/' }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to eq('Invalid URL provided')
|
||||
end
|
||||
|
||||
it 'blocks requests to localhost' do
|
||||
post upload_url,
|
||||
headers: user.create_new_auth_token,
|
||||
params: { external_url: 'http://localhost/secret' }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to eq('Invalid URL provided')
|
||||
end
|
||||
|
||||
it 'blocks requests to .local domains' do
|
||||
allow(Resolv).to receive(:getaddresses).with('server.local').and_return(['192.168.1.100'])
|
||||
|
||||
post upload_url,
|
||||
headers: user.create_new_auth_token,
|
||||
params: { external_url: 'http://server.local/secret' }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to eq('Invalid URL provided')
|
||||
end
|
||||
|
||||
it 'blocks DNS rebinding attacks (hostname resolving to private IP)' do
|
||||
allow(Resolv).to receive(:getaddresses).with('evil.attacker.com').and_return(['10.0.0.1'])
|
||||
|
||||
post upload_url,
|
||||
headers: user.create_new_auth_token,
|
||||
params: { external_url: 'http://evil.attacker.com/secret' }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to eq('Invalid URL provided')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns an error when no file or URL is provided' do
|
||||
|
||||
Reference in New Issue
Block a user