Skip to content

Commit 3674d31

Browse files
authored
Limit publisherId parameter length + tests. (#4762)
1 parent 8c5d7cc commit 3674d31

File tree

4 files changed

+124
-3
lines changed

4 files changed

+124
-3
lines changed

app/lib/frontend/handlers/account.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ Future<shelf.Response> unlikePackageHandler(
188188
/// Handles /api/account/options/publishers/<publisherId>
189189
Future<AccountPublisherOptions> accountPublisherOptionsHandler(
190190
shelf.Request request, String publisherId) async {
191+
checkPublisherIdParam(publisherId);
191192
final user = await requireAuthenticatedUser();
192193
final member =
193194
await publisherBackend.getPublisherMember(publisherId, user.userId);

app/lib/frontend/handlers/publisher.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ Future<shelf.Response> publisherListHandler(shelf.Request request) async {
4747
/// Handles requests for GET /publishers/<publisherId>
4848
Future<shelf.Response> publisherPageHandler(
4949
shelf.Request request, String publisherId) async {
50+
checkPublisherIdParam(publisherId);
5051
return redirectResponse(urls.publisherPackagesUrl(publisherId));
5152
}
5253

app/lib/publisher/backend.dart

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ import 'models.dart';
2222

2323
final _logger = Logger('pub.publisher.backend');
2424

25+
/// Sanity check to limit publisherId length.
26+
const maxPublisherIdLength = 64;
27+
2528
/// Sets the publisher backend service.
2629
void registerPublisherBackend(PublisherBackend backend) =>
2730
ss.register(#_publisherBackend, backend);
@@ -38,7 +41,7 @@ class PublisherBackend {
3841

3942
/// Loads a publisher (or returns null if it does not exists).
4043
Future<Publisher> getPublisher(String publisherId) async {
41-
ArgumentError.checkNotNull(publisherId, 'publisherId');
44+
checkPublisherIdParam(publisherId);
4245
final pKey = _db.emptyKey.append(Publisher, id: publisherId);
4346
return await _db.lookupValue<Publisher>(pKey, orElse: () => null);
4447
}
@@ -98,7 +101,7 @@ class PublisherBackend {
98101
/// Loads the [PublisherMember] instance for [userId] (or returns null if it does not exists).
99102
Future<PublisherMember> getPublisherMember(
100103
String publisherId, String userId) async {
101-
ArgumentError.checkNotNull(publisherId, 'publisherId');
104+
checkPublisherIdParam(publisherId);
102105
ArgumentError.checkNotNull(userId, 'userId');
103106
final mKey = _db.emptyKey
104107
.append(Publisher, id: publisherId)
@@ -108,6 +111,7 @@ class PublisherBackend {
108111

109112
/// Whether the User [userId] has admin permissions on the publisher.
110113
Future<bool> isMemberAdmin(String publisherId, String userId) async {
114+
checkPublisherIdParam(publisherId);
111115
ArgumentError.checkNotNull(publisherId, 'publisherId');
112116
if (userId == null) return false;
113117
final member = await getPublisherMember(publisherId, userId);
@@ -120,6 +124,7 @@ class PublisherBackend {
120124
String publisherId,
121125
api.CreatePublisherRequest body,
122126
) async {
127+
checkPublisherIdParam(publisherId);
123128
final user = await requireAuthenticatedUser();
124129
// Sanity check that domains are:
125130
// - lowercase (because we want that in pub.dev)
@@ -133,7 +138,7 @@ class PublisherBackend {
133138
InvalidInputException.checkStringLength(
134139
publisherId,
135140
'publisherId',
136-
maximum: 64, // Some upper limit for sanity.
141+
maximum: maxPublisherIdLength, // Some upper limit for sanity.
137142
);
138143
InvalidInputException.checkNotNull(body.accessToken, 'accessToken');
139144
InvalidInputException.checkStringLength(
@@ -207,6 +212,7 @@ class PublisherBackend {
207212

208213
/// Gets the publisher data
209214
Future<api.PublisherInfo> getPublisherInfo(String publisherId) async {
215+
checkPublisherIdParam(publisherId);
210216
final p = await getPublisher(publisherId);
211217
if (p == null) {
212218
throw NotFoundException('Publisher $publisherId does not exists.');
@@ -219,6 +225,7 @@ class PublisherBackend {
219225
/// Handles: `PUT /api/publishers/<publisherId>`
220226
Future<api.PublisherInfo> updatePublisher(
221227
String publisherId, api.UpdatePublisherRequest update) async {
228+
checkPublisherIdParam(publisherId);
222229
if (update.description != null) {
223230
// limit length, if not null
224231
InvalidInputException.checkStringLength(
@@ -301,6 +308,7 @@ class PublisherBackend {
301308

302309
/// Updates the contact email field of the publisher.
303310
Future updateContactEmail(String publisherId, String contactEmail) async {
311+
checkPublisherIdParam(publisherId);
304312
final activeUser = await requireAuthenticatedUser();
305313
await requirePublisherAdmin(publisherId, activeUser.userId);
306314
InvalidInputException.check(
@@ -323,6 +331,7 @@ class PublisherBackend {
323331
/// Invites a user to become a publisher admin.
324332
Future<account_api.InviteStatus> invitePublisherMember(
325333
String publisherId, api.InviteMemberRequest invite) async {
334+
checkPublisherIdParam(publisherId);
326335
final activeUser = await requireAuthenticatedUser();
327336
final p = await requirePublisherAdmin(publisherId, activeUser.userId);
328337
InvalidInputException.checkNotNull(invite.email, 'email');
@@ -354,6 +363,7 @@ class PublisherBackend {
354363
Future<List<api.PublisherMember>> listPublisherMembers(
355364
String publisherId,
356365
) async {
366+
checkPublisherIdParam(publisherId);
357367
final key = _db.emptyKey.append(Publisher, id: publisherId);
358368
// TODO: add caching
359369
final query = _db.query<PublisherMember>(ancestorKey: key);
@@ -370,6 +380,7 @@ class PublisherBackend {
370380
Future<api.PublisherMembers> handleListPublisherMembers(
371381
String publisherId,
372382
) async {
383+
checkPublisherIdParam(publisherId);
373384
final user = await requireAuthenticatedUser();
374385
await requirePublisherAdmin(publisherId, user.userId);
375386
return api.PublisherMembers(
@@ -380,6 +391,7 @@ class PublisherBackend {
380391
/// The list of email addresses of the members with admin roles. The list
381392
/// should be used to notify admins on upload events.
382393
Future<List<String>> getAdminMemberEmails(String publisherId) async {
394+
checkPublisherIdParam(publisherId);
383395
final key = _db.emptyKey.append(Publisher, id: publisherId);
384396
final query = _db.query<PublisherMember>(ancestorKey: key);
385397
final userIds = await query.run().map((m) => m.userId).toList();
@@ -389,6 +401,7 @@ class PublisherBackend {
389401
/// Returns the membership info of a user.
390402
Future<api.PublisherMember> publisherMemberInfo(
391403
String publisherId, String userId) async {
404+
checkPublisherIdParam(publisherId);
392405
final user = await requireAuthenticatedUser();
393406
final p = await requirePublisherAdmin(publisherId, user.userId);
394407
final key = p.key.append(PublisherMember, id: userId);
@@ -405,6 +418,7 @@ class PublisherBackend {
405418
String userId,
406419
api.UpdatePublisherMemberRequest update,
407420
) async {
421+
checkPublisherIdParam(publisherId);
408422
final user = await requireAuthenticatedUser();
409423
final p = await requirePublisherAdmin(publisherId, user.userId);
410424
final key = p.key.append(PublisherMember, id: userId);
@@ -436,6 +450,7 @@ class PublisherBackend {
436450

437451
/// Deletes a publisher's member.
438452
Future<void> deletePublisherMember(String publisherId, String userId) async {
453+
checkPublisherIdParam(publisherId);
439454
final user = await requireAuthenticatedUser();
440455
final p = await requirePublisherAdmin(publisherId, user.userId);
441456
if (userId == user.userId) {
@@ -460,6 +475,7 @@ class PublisherBackend {
460475
/// A callback from consent backend, when a consent is granted.
461476
/// Note: this will be retried when transaction fails due race conditions.
462477
Future<void> inviteConsentGranted(String publisherId, String userId) async {
478+
checkPublisherIdParam(publisherId);
463479
final user = await accountBackend.lookupUserById(userId);
464480
await withRetryTransaction(_db, (tx) async {
465481
final key = _db.emptyKey
@@ -536,3 +552,14 @@ Future purgePublisherCache({String publisherId}) async {
536552
}
537553

538554
String _publisherWebsite(String domain) => 'https://$domain/';
555+
556+
/// Verify that the [publisherId] parameter looks as acceptable input.
557+
void checkPublisherIdParam(String publisherId) {
558+
InvalidInputException.checkNotNull(publisherId, 'package');
559+
InvalidInputException.check(
560+
publisherId.trim() == publisherId, 'Invalid publisherId.');
561+
InvalidInputException.check(
562+
publisherId.contains('.'), 'Invalid publisherId.');
563+
InvalidInputException.checkStringLength(publisherId, 'publisherId',
564+
minimum: 3, maximum: 64);
565+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:io';
6+
7+
import 'package:test/test.dart';
8+
9+
import '../../shared/test_services.dart';
10+
11+
import '_utils.dart';
12+
13+
final _variableExp = RegExp(r'\<(.+?)\>');
14+
15+
void main() {
16+
// The test collects the GET routes containing a single `<publisherId>` named parameter.
17+
// These endpoints will be called with a combination of good and bad parameters, and
18+
// response status codes are checked against a valid set of codes specific to a use-case.
19+
final urls =
20+
_parseGetRoutes().where((url) => url.contains('<publisherId>')).where(
21+
(url) {
22+
final vars = _variableExp.allMatches(url).map((m) => m.group(1)).toSet();
23+
vars.remove('publisherId');
24+
return vars.isEmpty;
25+
},
26+
).toSet();
27+
28+
test('URLs are extracted', () {
29+
expect(urls, contains('/publishers/<publisherId>'));
30+
expect(urls, contains('/api/publishers/<publisherId>'));
31+
expect(urls, hasLength(6));
32+
});
33+
34+
testWithProfile(
35+
'Long URL check with <publisherId>',
36+
processJobsWithFakeRunners: true,
37+
fn: () async {
38+
Future<void> check(
39+
String kind,
40+
String Function(String url) fn,
41+
Set<int> expectedCodes,
42+
) async {
43+
final statusCodes = <int>{};
44+
for (final url in urls.toList()..sort()) {
45+
final u = fn(url);
46+
final rs = await issueGet(u);
47+
statusCodes.add(rs.statusCode);
48+
expect(rs.statusCode, lessThan(500), reason: '$u ${rs.statusCode}');
49+
expect(expectedCodes, contains(rs.statusCode),
50+
reason: '$kind $u ${rs.statusCode}');
51+
}
52+
expect(statusCodes, expectedCodes, reason: kind);
53+
}
54+
55+
await check(
56+
'existing publisherId',
57+
(url) => url.replaceAll('<publisherId>', 'example.com'),
58+
<int>{200, 303, 401},
59+
);
60+
61+
await check(
62+
'non-existing publisherId',
63+
(url) => url.replaceAll('<publisherId>', 'not-a-publisher.com'),
64+
<int>{303, 401, 404},
65+
);
66+
67+
await check(
68+
'publisherId longer than 64',
69+
(url) => url.replaceAll('<publisherId>', ('a' * 64) + '.com'),
70+
<int>{400},
71+
);
72+
73+
await check(
74+
'publisherId longer than 1500',
75+
(url) => url.replaceAll('<publisherId>', ('a' * 1500) + '.com'),
76+
<int>{400},
77+
);
78+
},
79+
);
80+
}
81+
82+
Iterable<String> _parseGetRoutes() sync* {
83+
final exp = RegExp(r"'GET',\s+r'(.*?)',");
84+
final files = [
85+
'lib/frontend/handlers/pubapi.g.dart',
86+
'lib/frontend/handlers/routes.g.dart',
87+
];
88+
for (final file in files) {
89+
final content = File(file).readAsStringSync();
90+
yield* exp.allMatches(content).map((m) => m.group(1));
91+
}
92+
}

0 commit comments

Comments
 (0)