Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions lib/models/BucketLoggingStatus.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { parseString } from 'xml2js';
import errors, { ArsenalError, errorInstances } from '../errors';
import escapeForXml from '../s3middleware/escapeForXml';

/** BucketLoggingStatus constants, not documented by AWS but found via testing */
const TARGET_BUCKET_MIN_LENGTH = 3;
Expand All @@ -10,7 +11,7 @@ const TARGET_PREFIX_MAX_LENGTH = 800;
* Format of xml request:
* https://docs.aws.amazon.com/AmazonS3/latest/API/API_LoggingEnabled.html
* https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketLogging.html
*
*
<?xml version="1.0" encoding="UTF-8"?>
<BucketLoggingStatus xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<LoggingEnabled>
Expand Down Expand Up @@ -61,7 +62,7 @@ export default class BucketLoggingStatus {
if (this._loggingEnabled) {
loggingEnabledXML = `<LoggingEnabled>
<TargetBucket>${this._loggingEnabled.TargetBucket}</TargetBucket>
<TargetPrefix>${this._loggingEnabled.TargetPrefix}</TargetPrefix>
<TargetPrefix>${escapeForXml(this._loggingEnabled.TargetPrefix)}</TargetPrefix>
</LoggingEnabled>
`;
}
Expand Down
6 changes: 3 additions & 3 deletions lib/models/LifecycleConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1250,13 +1250,13 @@ export default class LifecycleConfiguration {
}
const tags = filter && filter.tags;
const Prefix = rulePrefix !== undefined ?
`<Prefix>${rulePrefix}</Prefix>` : '';
`<Prefix>${escapeForXml(rulePrefix)}</Prefix>` : '';
let tagXML = '';
if (tags) {
tagXML = tags.map(t => {
const { key, val } = t;
const Tag = `<Tag><Key>${key}</Key>` +
`<Value>${val}</Value></Tag>`;
const Tag = `<Tag><Key>${escapeForXml(key)}</Key>` +
`<Value>${escapeForXml(val)}</Value></Tag>`;
return Tag;
}).join('');
}
Expand Down
9 changes: 5 additions & 4 deletions lib/models/NotificationConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from '../constants';
import { errorInstances } from '../errors';
import type { ArsenalError } from '../errors';
import escapeForXml from '../s3middleware/escapeForXml';

/**
* Format of xml request:
Expand Down Expand Up @@ -332,16 +333,16 @@ export default class NotificationConfiguration {
if (config && config.queueConfig) {
config.queueConfig.forEach(c => {
xmlArray.push('<QueueConfiguration>');
xmlArray.push(`<Id>${c.id}</Id>`);
xmlArray.push(`<Queue>${c.queueArn}</Queue>`);
xmlArray.push(`<Id>${escapeForXml(c.id)}</Id>`);
xmlArray.push(`<Queue>${escapeForXml(c.queueArn)}</Queue>`);
c.events.forEach(e => {
xmlArray.push(`<Event>${e}</Event>`);
});
if (c.filterRules) {
xmlArray.push('<Filter><S3Key>');
c.filterRules.forEach(r => {
xmlArray.push(`<FilterRule><Name>${r.name}</Name>` +
`<Value>${r.value}</Value></FilterRule>`);
xmlArray.push(`<FilterRule><Name>${escapeForXml(r.name)}</Name>` +
`<Value>${escapeForXml(r.value)}</Value></FilterRule>`);
});
xmlArray.push('</S3Key></Filter>');
}
Expand Down
24 changes: 24 additions & 0 deletions tests/unit/models/BucketLoggingStatus.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -306,5 +306,29 @@ describe('BucketLoggingStatus', () => {
assert.strictEqual(result.res.getLoggingEnabled(), undefined);
});
});

describe('XML escaping for special characters', () => {
const specialCharacters = ['&', '<', '>', '"', "'"];

specialCharacters.forEach(char =>
it(`should escape \`${char}\` in TargetPrefix and generate valid XML`, done => {
const loggingEnabled = {
TargetBucket: 'test-bucket',
TargetPrefix: `logs/app${char}env/`,
};
const config = new BucketLoggingStatus(loggingEnabled);
const xml = config.toXML();

// Verify the XML is valid and the character roundtrips by parsing it
parseString(xml, { explicitArray: false }, (err, result) => {
assert.ifError(err);
assert(result.BucketLoggingStatus);
const logging = result.BucketLoggingStatus.LoggingEnabled;
assert.strictEqual(logging.TargetPrefix, `logs/app${char}env/`);
done();
});
})
);
});
});
});
82 changes: 82 additions & 0 deletions tests/unit/models/LifecycleConfiguration.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1396,3 +1396,85 @@ describe('LifecycleConfiguration::getConfigJson', () => {
);
}));
});

describe('LifecycleConfiguration.getConfigXml - XML escaping for special characters', () => {
const specialCharacters = ['&', '<', '>', '"', "'"];

specialCharacters.forEach(char => {
it(`should escape \`${char}\` in rule ID and generate valid XML`, done => {
const config = {
rules: [{
ruleID: `test-id${char}value`,
ruleStatus: 'Enabled',
prefix: 'logs/',
actions: [{
actionName: 'Expiration',
days: 90,
}],
}],
};

const xml = LifecycleConfiguration.getConfigXml(config);

parseString(xml, (err, result) => {
assert.ifError(err);
const rule = result.LifecycleConfiguration.Rule[0];
assert.strictEqual(rule.ID[0], `test-id${char}value`);
done();
});
});

it(`should escape \`${char}\` in prefix`, done => {
const config = {
rules: [{
ruleID: 'test-id',
ruleStatus: 'Enabled',
prefix: `logs/${char}path/`,
actions: [{
actionName: 'Expiration',
days: 90,
}],
}],
};

const xml = LifecycleConfiguration.getConfigXml(config);

parseString(xml, (err, result) => {
assert.ifError(err);
const rule = result.LifecycleConfiguration.Rule[0];
assert.strictEqual(rule.Prefix[0], `logs/${char}path/`);
done();
});
});

it(`should escape \`${char}\` in tag key and value`, done => {
const config = {
rules: [{
ruleID: 'test-id',
ruleStatus: 'Enabled',
filter: {
tags: [{
key: `env${char}key`,
val: `value${char}data`,
}],
},
actions: [{
actionName: 'Expiration',
days: 90,
}],
}],
};

const xml = LifecycleConfiguration.getConfigXml(config);

parseString(xml, (err, result) => {
assert.ifError(err);
const rule = result.LifecycleConfiguration.Rule[0];
const tag = rule.Filter[0].Tag[0];
assert.strictEqual(tag.Key[0], `env${char}key`);
assert.strictEqual(tag.Value[0], `value${char}data`);
done();
});
});
});
});
71 changes: 71 additions & 0 deletions tests/unit/models/NotificationConfiguration.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -290,3 +290,74 @@ describe('NotificationConfiguration.restrictSupportedNotificationBasedOnLifecycl
expect(supportedNotificationEvents.has('s3:ObjectCreated:*')).toBeTruthy();
});
});

describe('NotificationConfiguration.getConfigXML - XML escaping for special characters', () => {
const specialCharacters = ['&', '<', '>', '"', "'"];

specialCharacters.forEach(char => {
it(`should escape \`${char}\` in notification ID and generate valid XML`, done => {
const config = {
queueConfig: [{
id: `test-id${char}value`,
queueArn: 'arn:scality:bucketnotif:::target',
events: ['s3:ObjectCreated:*'],
filterRules: [],
}],
};

const xml = NotificationConfiguration.getConfigXML(config);

parseString(xml, (err, result) => {
assert.ifError(err);
const queueConfig = result.NotificationConfiguration.QueueConfiguration[0];
assert.strictEqual(queueConfig.Id[0], `test-id${char}value`);
done();
});
});

it(`should escape \`${char}\` in queue ARN`, done => {
const config = {
queueConfig: [{
id: 'test-id',
queueArn: `arn:scality:bucketnotif:::queue${char}name`,
events: ['s3:ObjectCreated:*'],
filterRules: [],
}],
};

const xml = NotificationConfiguration.getConfigXML(config);

parseString(xml, (err, result) => {
assert.ifError(err);
const queueConfig = result.NotificationConfiguration.QueueConfiguration[0];
assert.strictEqual(queueConfig.Queue[0], `arn:scality:bucketnotif:::queue${char}name`);
done();
});
});

it(`should escape \`${char}\` in filter rule name and value`, done => {
const config = {
queueConfig: [{
id: 'test-id',
queueArn: 'arn:scality:bucketnotif:::target',
events: ['s3:ObjectCreated:*'],
filterRules: [{
name: `Prefix${char}Name`,
value: `logs/${char}path`,
}],
}],
};

const xml = NotificationConfiguration.getConfigXML(config);

parseString(xml, (err, result) => {
assert.ifError(err);
const queueConfig = result.NotificationConfiguration.QueueConfiguration[0];
const filterRule = queueConfig.Filter[0].S3Key[0].FilterRule[0];
assert.strictEqual(filterRule.Name[0], `Prefix${char}Name`);
assert.strictEqual(filterRule.Value[0], `logs/${char}path`);
done();
});
});
});
});
Loading