Skip to content

Commit e5719a2

Browse files
committed
Validate ephermeral blobstore id format
Add shared UuidValidation helper matching SecureRandom.uuid format. AgentClient only fetches ephemeral blobs when the id is a well-formed UUID; invalid ids are logged and skipped. LocalClient restricts filesystem paths to UUID object names. Reject non-UUID ids returned when fetching logs without signed URLs and when persisting compiled package task results without signed URLs. Introduce AgentTaskInvalidBlobstoreId for log fetch responses and PackageCompilationInvalidTaskBlobstoreId for compile task results. Update unit tests for AgentClient, LocalClient, and the new helper. Made-with: Cursor
1 parent dc39d46 commit e5719a2

14 files changed

Lines changed: 228 additions & 63 deletions

File tree

src/bosh-director/lib/bosh/director/agent_client.rb

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require 'bosh/director/agent_message_converter'
2+
require 'bosh/director/blobstore/uuid_validation'
23

34
module Bosh::Director
45
class AgentClient
@@ -317,8 +318,10 @@ def format_exception(exception)
317318

318319
if exception['blobstore_id']
319320
blob = download_and_delete_blob(exception['blobstore_id'])
320-
msg += "\n"
321-
msg += blob.to_s
321+
unless blob.nil?
322+
msg += "\n"
323+
msg += blob.to_s
324+
end
322325
end
323326

324327
msg
@@ -334,18 +337,27 @@ def inject_compile_log(response)
334337
response['value']['result'].is_a?(Hash) &&
335338
(blob_id = response['value']['result']['compile_log_id'])
336339
compile_log = download_and_delete_blob(blob_id)
337-
response['value']['result']['compile_log'] = compile_log
340+
response['value']['result']['compile_log'] = compile_log unless compile_log.nil?
338341
end
339342
end
340343

341344
# Downloads blob and ensures it's deleted from the blobstore
342345
# @param [String] blob_id Blob id
343346
# @return [String] Blob contents
344347
def download_and_delete_blob(blob_id)
348+
unless Blobstore::UuidValidation.valid_uuid?(blob_id)
349+
@logger.warn(
350+
"Skipping blob fetch for agent '#{@client_id}' (#{@instance_name}): object id is not a valid UUID",
351+
)
352+
return nil
353+
end
354+
345355
blob = @resource_manager.get_resource(blob_id)
346356
blob
347357
ensure
348-
@resource_manager.delete_resource(blob_id)
358+
if blob_id && Blobstore::UuidValidation.valid_uuid?(blob_id)
359+
@resource_manager.delete_resource(blob_id)
360+
end
349361
end
350362

351363
def handle_message_with_retry(message_name, *args, &blk)

src/bosh-director/lib/bosh/director/api/controllers/deployments_controller.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,11 @@ def initialize(config)
490490
manifest_text = request.body.read
491491
manifest_hash = validate_manifest_yml(manifest_text)
492492

493-
manifest_hash['name'] = deployment.name if deployment
493+
if deployment
494+
manifest_hash['name'] = deployment.name
495+
# ensure diff will show `name` from `deployment.name` and not manifest YAML
496+
manifest_text = YAML.dump(manifest_hash)
497+
end
494498

495499
if deployment
496500
before_manifest = Manifest.load_from_model(deployment, resolve_interpolation: false)

src/bosh-director/lib/bosh/director/blobstore/local_client.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
require 'bosh/director/blobstore/uuid_validation'
2+
13
module Bosh::Director
24
module Blobstore
35
class LocalClient < Client
@@ -52,6 +54,10 @@ def required_credential_properties_list
5254
private
5355

5456
def object_file_path(oid)
57+
unless UuidValidation.valid_uuid?(oid)
58+
raise BlobstoreError, "invalid blobstore object id format: #{oid.inspect}"
59+
end
60+
5561
File.join(@blobstore_path, oid)
5662
end
5763
end
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module Bosh::Director
2+
module Blobstore
3+
# Validates UUIDs are (8-4-4-4-12 hex):
4+
# - Ruby: SecureRandom.uuid
5+
# - Golang: github.com/nu7hatch/gouuid
6+
module UuidValidation
7+
UUID_PATTERN = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i.freeze
8+
9+
def self.valid_uuid?(value)
10+
UUID_PATTERN.match?(value.to_s)
11+
end
12+
end
13+
end
14+
end

src/bosh-director/lib/bosh/director/deployment_plan/stages/package_compile_stage.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
require 'bosh/director/blobstore/uuid_validation'
12
require 'bosh/director/compiled_package_requirement_generator'
23
require 'digest/sha1'
34

@@ -131,6 +132,8 @@ def compile_package(requirement)
131132
end
132133
end
133134

135+
validate_compiled_package_blobstore_id!(task_result['blobstore_id'])
136+
134137
compiled_package = Models::CompiledPackage.create do |p|
135138
p.package = package
136139
p.stemcell_os = stemcell.os
@@ -158,6 +161,13 @@ def prepare_vm(...)
158161

159162
private
160163

164+
def validate_compiled_package_blobstore_id!(blobstore_id)
165+
return if Blobstore::UuidValidation.valid_uuid?(blobstore_id)
166+
167+
raise PackageCompilationInvalidTaskBlobstoreId,
168+
'Compilation task result contained an invalid blobstore object id'
169+
end
170+
161171
def validate_packages(instance_groups_to_compile)
162172
instance_groups_to_compile.each do |instance_group|
163173
instance_group.jobs.each do |job|

src/bosh-director/lib/bosh/director/errors.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ def self.err(error_code, response_code = BAD_REQUEST)
287287
AgentInvalidTaskResult = err(400011)
288288
AgentUnsupportedAction = err(400012)
289289
AgentUploadBlobUnableToOpenFile = err(400013)
290+
AgentTaskInvalidBlobstoreId = err(400014)
290291

291292
# Cloud check task errors
292293
CloudcheckTooManySimilarProblems = err(410001)
@@ -298,6 +299,7 @@ def self.err(error_code, response_code = BAD_REQUEST)
298299
DnsInvalidCanonicalName = err(420001)
299300

300301
# PackageCompilation
302+
PackageCompilationInvalidTaskBlobstoreId = err(430004)
301303
PackageCompilationNotEnoughWorkersForReuse = err(430002)
302304
PackageCompilationNotFound = err(430003)
303305

src/bosh-director/lib/bosh/director/logs_fetcher.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
require 'bosh/director/blobstore/uuid_validation'
2+
13
module Bosh::Director
24
class LogsFetcher
35
# @param [Bosh::Director::EventLog::Log] event_log
@@ -41,6 +43,11 @@ def fetch(instance, log_type, filters, persist_blobstore_id = false)
4143
raise AgentTaskNoBlobstoreId,
4244
"Agent didn't return a blobstore object id for packaged logs"
4345
end
46+
47+
unless Blobstore::UuidValidation.valid_uuid?(blobstore_id)
48+
raise AgentTaskInvalidBlobstoreId,
49+
"Agent didn't return a valid blobstore object id for packaged logs"
50+
end
4451
end
4552
sha_digest = fetch_logs_result['sha1']
4653
end

src/bosh-director/spec/unit/bosh/director/agent_client_spec.rb

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -549,20 +549,21 @@ def self.it_acts_as_message_with_timeout(message_name)
549549
end
550550

551551
it 'should handle exceptions' do
552+
blob_uuid = 'c0eebc99-9c0b-4ef8-bb6d-6bb9bd380a13'
552553
response = {
553554
'exception' => {
554555
'message' => 'test',
555556
'backtrace' => %w[a b c],
556-
'blobstore_id' => 'deadbeef',
557+
'blobstore_id' => blob_uuid,
557558
},
558559
}
559560

560561
expect(@nats_rpc).to receive(:send_request)
561562
.with('foo.bar', 'bar', expected_rpc_args, options).and_yield(response)
562563

563564
rm = double(Bosh::Director::Api::ResourceManager)
564-
expect(rm).to receive(:get_resource).with('deadbeef').and_return('an exception')
565-
expect(rm).to receive(:delete_resource).with('deadbeef')
565+
expect(rm).to receive(:get_resource).with(blob_uuid).and_return('an exception')
566+
expect(rm).to receive(:delete_resource).with(blob_uuid)
566567
expect(Bosh::Director::Api::ResourceManager).to receive(:new).and_return(rm)
567568

568569
client = AgentClient.new('foo', 'bar', 'foo_instance/1')
@@ -715,11 +716,13 @@ def self.it_acts_as_message_with_timeout(message_name)
715716
end
716717

717718
describe 'handling compilation log' do
719+
let(:compile_log_uuid) { 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11' }
720+
718721
it 'should inject compile log into response' do
719722
response = {
720723
'value' => {
721724
'result' => {
722-
'compile_log_id' => 'cafe',
725+
'compile_log_id' => compile_log_uuid,
723726
},
724727
},
725728
}
@@ -728,14 +731,36 @@ def self.it_acts_as_message_with_timeout(message_name)
728731
.with('foo.bar', 'bar', expected_rpc_args, options).and_yield(response)
729732

730733
rm = instance_double('Bosh::Director::Api::ResourceManager')
731-
expect(rm).to receive(:get_resource).with('cafe').and_return('blob')
732-
expect(rm).to receive(:delete_resource).with('cafe')
734+
expect(rm).to receive(:get_resource).with(compile_log_uuid).and_return('blob')
735+
expect(rm).to receive(:delete_resource).with(compile_log_uuid)
733736
expect(Bosh::Director::Api::ResourceManager).to receive(:new).and_return(rm)
734737

735738
client = AgentClient.new('foo', 'bar', 'foo_instance/1')
736739
value = client.baz(*test_args)
737740
expect(value['result']['compile_log']).to eq('blob')
738741
end
742+
743+
it 'does not fetch compile log when compile_log_id is not a UUID' do
744+
response = {
745+
'value' => {
746+
'result' => {
747+
'compile_log_id' => 'cafe',
748+
},
749+
},
750+
}
751+
752+
expect(@nats_rpc).to receive(:send_request)
753+
.with('foo.bar', 'bar', expected_rpc_args, options).and_yield(response)
754+
755+
rm = instance_double('Bosh::Director::Api::ResourceManager')
756+
expect(rm).not_to receive(:get_resource)
757+
expect(rm).not_to receive(:delete_resource)
758+
expect(Bosh::Director::Api::ResourceManager).to receive(:new).and_return(rm)
759+
760+
client = AgentClient.new('foo', 'bar', 'foo_instance/1')
761+
value = client.baz(*test_args)
762+
expect(value['result']).not_to have_key('compile_log')
763+
end
739764
end
740765

741766
describe 'formatting RPC remote exceptions' do
@@ -745,23 +770,39 @@ def self.it_acts_as_message_with_timeout(message_name)
745770
end
746771

747772
it 'supports new style (Hash)' do
773+
blob_uuid = 'b0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12'
748774
exception = {
749775
'message' => 'something happened',
750776
'backtrace' => ['in zbb.rb:35', 'in zbb.rb:26'],
751-
'blobstore_id' => 'deadbeef',
777+
'blobstore_id' => blob_uuid,
752778
}
753779

754780
rm = instance_double('Bosh::Director::Api::ResourceManager')
755781
allow(Bosh::Director::Api::ResourceManager).to receive(:new).and_return(rm)
756-
expect(rm).to receive(:get_resource).with('deadbeef')
782+
expect(rm).to receive(:get_resource).with(blob_uuid)
757783
.and_return("Failed to compile: no such file 'zbb'")
758-
expect(rm).to receive(:delete_resource).with('deadbeef')
784+
expect(rm).to receive(:delete_resource).with(blob_uuid)
759785

760786
expected_error = "something happened\nin zbb.rb:35\nin zbb.rb:26\nFailed to compile: no such file 'zbb'"
761787

762788
client = AgentClient.new('foo', 'bar', 'foo_instance/1')
763789
expect(client.format_exception(exception)).to eq(expected_error)
764790
end
791+
792+
it 'does not append blob content when blobstore_id is not a UUID' do
793+
exception = {
794+
'message' => 'something happened',
795+
'blobstore_id' => 'deadbeef',
796+
}
797+
798+
rm = instance_double('Bosh::Director::Api::ResourceManager')
799+
allow(Bosh::Director::Api::ResourceManager).to receive(:new).and_return(rm)
800+
expect(rm).not_to receive(:get_resource)
801+
expect(rm).not_to receive(:delete_resource)
802+
803+
client = AgentClient.new('foo', 'bar', 'foo_instance/1')
804+
expect(client.format_exception(exception)).to eq('something happened')
805+
end
765806
end
766807

767808
describe '#run_errand' do

src/bosh-director/spec/unit/bosh/director/api/controllers/deployments_controller_spec.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2737,10 +2737,14 @@ def perform
27372737

27382738
it 'uses the deployment from the URL when the YAML manifest names a different deployment' do
27392739
manifest = YAML.dump('name' => 'other_deployment', 'releases' => [], 'instance_groups' => [])
2740+
expect_any_instance_of(DeploymentManager)
2741+
.to receive(:create_deployment) do |_, _, rewritten_manifest, *_|
2742+
expect(YAML.load(rewritten_manifest)).to include('name' => 'owned_deployment')
2743+
OpenStruct.new(id: 1)
2744+
end
27402745
put '/owned_deployment/jobs/dea?state=recreate', manifest, { 'CONTENT_TYPE' => 'text/yaml' }
27412746
expect(last_response.status).to eq(302)
27422747
end
2743-
27442748
end
27452749

27462750
context 'PUT /:deployment/jobs/:job/:index_or_id' do

0 commit comments

Comments
 (0)